Go GraphQL 教程

一般的 Web 開發都是使用 RESTful 風格進行 API 的開發,這種 RESTful 風格的 API 開發的一般流程是:

一種資源一般都可以抽象出 4 類路由,比如投票接口:

# 獲取所有投票信息
GET /v1/api/votes

# 獲取單個投票信息
GET /v1/api/vote/{vote_id}

# 創建投票
POST /v1/api/vote

# 更新投票
PATCH  /v1/api/vote/{vote_id}

# 刪除投票
DELETE /v1/api/vote/{vote_id}

分別對應資源的獲取、創建、更新、刪除。

對於後端開發人員而言,重要的是在滿足需求的前提下設計這類 API。

設計這類 API 一般需要處理這些具體的問題:

前端或者客戶端,根據具體的需求,調用接口,對接口返回的字段進行處理。儘管有時候需求並不需要所有字段,又或者有時候需求需要 調用多個接口,組裝成一個大的格式,以完成需求。

後端抽象出多少實體,對應就會設計各種資源實體的接口。後續需求變更,爲了兼容,需要維護越來越多的接口。

看到沒,這類的接口設計:

GraphQL API

GraphQL 是一種專門用於 API 的查詢語言,由大廠 Facebook 推出,但是至今 GraphQL 並沒有引起廣泛的使用, 絕大多少還是採用 RESTful API 風格的形式開發。

GraphQL 嘗試解決這些問題:

1

既然是一種專門用於 API 的查詢語言,其必定有一些規範或者語法約束。具體 GraphQL 包含哪些知識呢?

schema.graphql

type Query {
    ping(data: String): Pong
}

type Mutation {
    createVote(name: String!): Vote
}

type Pong{
    data: String
    code: Int
}

type Vote {
    id: ID!
    name: String!
}

具體定義了請求合集:Query, 更改或者創建合集:Mutation,定義了兩個對象類型:Pong, Vote , 對象內包含字段和類型。

這個 schema 文件,是後端開發人員的開發文檔,也是前端或者客戶端人員的 API 文檔。

假設,後端開發人員依據 schema 文件,已經開發完畢,那麼如何調用 API 呢?

推薦使用:Postman

# ping 請求動作
query {
    ping{
        data
        code
    }
}
# mutation 更改動作
mutation {
    createVote(name:"have a lunch") {
        id
        name 
    }
}

能發現一些規律麼?

query HeartBeat {
    ping{
        data
        code
    }
}

GraphQL 是一種專門用於 API 的查詢語言,有語法約束。

具體包括:

講了這麼些,其實最好的方式還是親自調用下接口,參照着官方文檔,按個調用嘗試下,熟悉這套語法規範。

最佳的當然是:Github 的 GraphQL API4 (https://developer.github.com/v4/)

登陸自己的賬號:訪問:https://developer.github.com/v4/explorer/

僅舉幾個示例:

0. viewer: User!

1. 基本請求動作

{
  viewer {
    __typename
    ... on User {
      name
    }
  }
}

// 結果

{
  "data"{
    "viewer"{
      "__typename""User",
      "name""XieWei"
    }
  }
}

2. 別名

{
  AliasForViewer:viewer {
    __typename
    ... on User {
      name
    }
  }
}


# 結果
{
  "data"{
    "AliasForViewer"{
      "__typename""User",
      "name""XieWei"
    }
  }
}

3. 操作名稱,變量,指令

query PrintViewer($Repository: String!,$Has: Boolean!){
  AliasForViewer:viewer{
    __typename
    ... on User {
      name
    }
    url
    status{
      createdAt
      emoji
      id
    }
    repository(name: $Repository) {
      name
      createdAt
      description @include(if:$Has)

    }

  }
}

# 變量
{
  "Repository""2019-daily",
  "Has"false
}

# 結果

{
  "data"{
    "AliasForViewer"{
      "__typename""User",
      "name""XieWei",
      "url""https://github.com/wuxiaoxiaoshen",
      "status": null,
      "repository"{
        "name""2019-daily",
        "createdAt""2019-01-11T15:17:43Z"
      }
    }
  }
}

# 如果變量爲:

{
  "Repository""2019-daily",
  "Has"true
}

# 則結果爲

{
  "data"{
    "AliasForViewer"{
      "__typename""User",
      "name""XieWei",
      "url""https://github.com/wuxiaoxiaoshen",
      "status": null,
      "repository"{
        "name""2019-daily",
        "createdAt""2019-01-11T15:17:43Z",
        "description""把2019年的生活過成一本書"
      }
    }
  }
}

對照着文檔多嘗試。

上文多是講述使用 GraphQL 進行查詢操作時的語法。

2

schema 是所有請求、響應、對象聲明的集合,對後端而言,是開發依據,對前端而言,是 API 文檔。

如何定義 schema ?

你只需要知道這些內容即可:

舉一個具體的示例:小程序:騰訊投票

首頁

詳情

Step1: 定義類型對象的字段

定義的類型對象和響應的字段設計幾乎保持一致。

# 類似於 map, 左邊表示字段名稱,右邊表示類型
# [] 表示列表
# ! 修飾符表示非空
type Vote {
    id: ID!
    createdAt: Time
    updatedAt: Time
    deletedAt: Time
    title: String
    description: String
    options: [Options!]!
    deadline: Time
    class: VoteClass
}

type Options {
    name: String
}

# 輸入類型: 一般用戶更改資源中的輸入是列表對象,完成複雜任務
input optionsInput {
    name:String!
}

# 枚舉類型:投票區分:單選、多選兩個選項值
enum VoteClass {
    SINGLE
    MULTIPLE
}

# 自定義類型,默認類型(ID、String、Boolean、Float)不包含 Time 類型
scalar Time

# 對象類型,用於檢查服務是否完好
type Ping {
    data: String
    code: Int

}

Step2: 定義操作類型:Query 用於查詢,Mutation 用於創建、更改、刪除資源

# Query、Mutation 關鍵字固定
# 左邊表示操作名稱,右邊表示返回的值的類型
# Query 一般完成查詢操作
# Mutation 一般完成資源的創建、更改、刪除操作

type Query {
    ping: Ping
    pinWithData(data: String): Ping
    vote(id:ID!): Vote
}

type Mutation {
    createVote(title:String!, options:[optionsInput],deadline:Time, description:String, class:VoteClass!): Vote
    updateVote(title:String!, description:String!): Vote
}

schema 完成了對對象類型的定義和一些操作,是後端開發者的開發文檔,是前端開發者的 API 文檔。

3

客戶端如何使用:Go : (graphql-go)

主題:小程序騰訊投票

Step0: 項目結構

├── Makefile
├── README.md
├── cmd
│   ├── root_cmd.go
│   └── sync_cmd.go
├── main.go
├── model
│   └── vote.go
├── pkg
│   ├── database
│   │   └── database.go
│   └── router
│       └── router.go
├── schema.graphql
├── script
│   └── db.sh
└── web
    ├── mutation
    │   └── mutation_type.go
    ├── ping
    │   └── ping_query.go
    ├── query
    │   └── query_type.go
    └── vote
        ├── vote_curd.go
        ├── vote_params.go
        └── vote_type.go

和之前的 RESTful API 的設計項目的結構基本保持一致。

Step1: 依據 Schema 的定義:完成數據庫模型定義

type base struct {
    Id        int64      `xorm:"pk autoincr notnull" json:"id"`
    CreatedAt time.Time  `xorm:"created" json:"created_at"`
    UpdatedAt time.Time  `xorm:"updated" json:"updated_at"`
    DeletedAt *time.Time `xorm:"deleted" json:"deleted_at"`
}

const (
    SINGLE = iota
    MULTIPLE
)

var ClassMap = map[int]string{}

func init() {
    ClassMap = make(map[int]string)
    ClassMap[SINGLE] = "SINGLE"
    ClassMap[MULTIPLE] = "MULTIPLE"
}

type Vote struct {
    base        `xorm:"extends"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    OptionIds   []int64   `json:"option_ids"`
    Deadline    time.Time `json:"deadline"`
    Class       int       `json:"class"`
}

type VoteSerializer struct {
    Id          int64              `json:"id"`
    CreatedAt   time.Time          `json:"created_at"`
    UpdatedAt   time.Time          `json:"updated_at"`
    Title       string             `json:"title"`
    Description string             `json:"description"`
    Options     []OptionSerializer `json:"options"`
    Deadline    time.Time          `json:"deadline"`
    Class       int                `json:"class"`
    ClassString string             `json:"class_string"`
}

func (V Vote) TableName() string {
    return "votes"
}

func (V Vote) Serializer() VoteSerializer {
    var optionSerializer []OptionSerializer
    var options []Option
    database.Engine.In("id", V.OptionIds).Find(&options)
    for _, i := range options {
        optionSerializer = append(optionSerializer, i.Serializer())
    }
    classString := func(value int) string {
        if V.Class == SINGLE {
            return "單選"
        }
        if V.Class == MULTIPLE {
            return "多選"
        }
        return ""
    }
    return VoteSerializer{
        Id:          V.Id,
        CreatedAt:   V.CreatedAt.Truncate(time.Second),
        UpdatedAt:   V.UpdatedAt.Truncate(time.Second),
        Title:       V.Title,
        Description: V.Description,
        Options:     optionSerializer,
        Deadline:    V.Deadline,
        Class:       V.Class,
        ClassString: classString(V.Class),
    }
}

type Option struct {
    base `xorm:"extends"`
    Name string `json:"name"`
}

type OptionSerializer struct {
    Id        int64     `json:"id"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
    Name      string    `json:"name"`
}

func (O Option) TableName() string {
    return "options"
}

func (O Option) Serializer() OptionSerializer {
    return OptionSerializer{
        Id:        O.Id,
        CreatedAt: O.CreatedAt.Truncate(time.Second),
        UpdatedAt: O.UpdatedAt.Truncate(time.Second),
        Name:      O.Name,
    }
}

依然保持了個人的模型設計風格:

Step2: query.go 文件描述

var Query = graphql.NewObject(graphql.ObjectConfig{
    Name: "Query",
    Fields: graphql.Fields{
        "ping"&graphql.Field{
            Type: ping.Ping,
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                return ping.Default, nil
            },
        },
    },
})

func init() {
    Query.AddFieldConfig("pingWithData"&graphql.Field{
        Type: ping.Ping,
        Args: graphql.FieldConfigArgument{
            "data"&graphql.ArgumentConfig{
                Type: graphql.NewNonNull(graphql.String),
            },
        },
        Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
            if p.Args["data"] == nil {
                return ping.Default, nil
            }
            return ping.MakeResponseForPing(p.Args["data"].(string)), nil
        },
    })
}

func init() {
    Query.AddFieldConfig("vote"&graphql.Field{
        Type: vote.Vote,
        Args: graphql.FieldConfigArgument{
            "id"&graphql.ArgumentConfig{
                Type: graphql.NewNonNull(graphql.ID),
            },
        },
        Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
            id := p.Args["id"]
            ID, _ := strconv.Atoi(id.(string))
            return vote.GetOneVote(int64(ID))
        },
    })
}

基本和 schema 文件中 Query 定義一致:

type Query {
    ping: Ping
    pinWithData(data: String): Ping
    vote(id:ID!): Vote
}

內置類型:(ID, String, Boolean, Float)

- graphql.ID
- graphql.String
- graphql.Boolean
- graphql.Float
...

簡單的說:所有的對象、字段都需要有處理函數。

var Query = graphql.NewObject(graphql.ObjectConfig{
    Name: "Query",
    Fields: graphql.Fields{
        "ping"&graphql.Field{
            Type: ping.Ping,
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                return ping.Default, nil
            },
        },
    },
})

func init() {
    Query.AddFieldConfig("pingWithData"&graphql.Field{
        Type: ping.Ping,
        Args: graphql.FieldConfigArgument{
            "data"&graphql.ArgumentConfig{
                Type: graphql.NewNonNull(graphql.String),
            },
        },
        Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
            if p.Args["data"] == nil {
                return ping.Default, nil
            }
            return ping.MakeResponseForPing(p.Args["data"].(string)), nil
        },
    })
}

var Ping = graphql.NewObject(graphql.ObjectConfig{
    Name: "ping",
    Fields: graphql.Fields{
        "data"&graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                if response, ok := p.Source.(ResponseForPing); ok {
                    return response.Data, nil
                }
                return nil, fmt.Errorf("field not found")
            },
        },
        "code"&graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                if response, ok := p.Source.(ResponseForPing); ok {
                    return response.Code, nil
                }
                return nil, fmt.Errorf("field not found")
            },
        },
    },
})

type ResponseForPing struct {
    Data string `json:"data"`
    Code int    `json:"code"`
}

var Default = ResponseForPing{
    Data: "pong",
    Code: http.StatusOK,
}

func MakeResponseForPing(data string) ResponseForPing {
    return ResponseForPing{
        Data: data,
        Code: http.StatusOK,
    }
}

使用 Go Graphql-go 客戶端,絕大多數工作都在定義對象、定義字段類型、定義字段的處理函數等。

Step3: mutation.go 文件描述

var Mutation = graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
        "createVote"&graphql.Field{
            Type: vote.Vote,
            Args: graphql.FieldConfigArgument{
                "title"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "options"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
                },
                "description"&graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "deadline"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "class"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(vote.Class),
                },
            },
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                log.Println(p.Args)
                var params vote.CreateVoteParams
                params.Title = p.Args["title"].(string)
                if p.Args["description"] != nil {
                    params.Description = p.Args["description"].(string)
                }
                params.Deadline = p.Args["deadline"].(string)
                params.Class = p.Args["class"].(int)
                var options []vote.OptionParams
                for _, i := range p.Args["options"].([]interface{}) {
                    var one vote.OptionParams
                    k := i.(map[string]interface{})
                    one.Name = k["name"].(string)
                    options = append(options, one)
                }
                params.Options = options
                log.Println(params)
                result, err := vote.CreateVote(params)
                if err != nil {
                    return nil, err
                }
                return result, nil

            },
        },
        "updateVote"&graphql.Field{
            Type: vote.Vote,
            Args: graphql.FieldConfigArgument{
                "title"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "description"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "id"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.ID),
                },
            },
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                var params vote.UpdateVoteParams
                id := p.Args["id"]
                ID, _ := strconv.Atoi(id.(string))
                params.Id = int64(ID)
                params.Title = p.Args["title"].(string)
                params.Description = p.Args["description"].(string)
                return vote.UpdateOneVote(params)
            },
        },
    },
})

Step4: 構建 schema 啓動服務

func RegisterSchema() *graphql.Schema {
    schema, err := graphql.NewSchema(
        graphql.SchemaConfig{
            Query:    query.Query,
            Mutation: mutation.Mutation,
        })
    if err != nil {
        panic(fmt.Sprintf("schema init fail %s", err.Error()))
    }
    return &schema

}

func Register() *handler.Handler {
    return handler.New(&handler.Config{
        Schema:   RegisterSchema(),
        Pretty:   true,
        GraphiQL: true,
    })
}
func StartWebServer() {
    log.Println("Start Web Server...")
    http.Handle("/graphql", Register())
    log.Fatal(http.ListenAndServe(":7878", nil))
}

Step5: 運行,接口調用

接口調用示例:(根據查詢文檔,可以根據調用者的需求,自主選擇響應的字段)

mutation {
    createVote(
        title: "去哪玩?",
        description:"本次團建去哪玩?",
        options:[
            {
                name: "杭州西湖"
            },{
                name:"安徽黃山"
            },{
                name:"香港九龍"
            }
            ],
        deadline: "2019-08-01 00:00:00",
        class: SINGLE
        ) {
            id
            title
            deadline
            description
            createdAt
            updatedAt
            options{
                name
            }
            class
            classString
        }
}

# 結果

{
    "data"{
        "vote"{
            "class""SINGLE",
            "classString""單選",
            "createdAt""2019-07-30T19:33:27+08:00",
            "deadline""2019-08-01T00:00:00+08:00",
            "description""本次團建去哪玩?",
            "id""1",
            "options"[
                {
                    "name""杭州西湖"
                },
                {
                    "name""安徽黃山"
                },
                {
                    "name""香港九龍"
                }
            ],
            "title""去哪玩?",
            "updatedAt""2019-07-30T19:33:27+08:00"
        }
    }
}
query{
    vote(id:1){
            id
            title
            deadline
            description
            createdAt
            updatedAt
            options{
                name
            }
            class
            classString
    }
}

# 結果

{
    "data"{
        "createVote"{
            "class""SINGLE",
            "classString""SINGLE",
            "createdAt""2019-07-30T19:33:27+08:00",
            "deadline""2019-08-01T00:00:00+08:00",
            "description""本次團建去哪玩?",
            "id""1",
            "options"{
                {
                    "name""杭州西湖"
                },
                {
                    "name""安徽黃山"
                },
                {
                    "name""香港九龍"
                }
            },
            "title""去哪玩?",
            "updatedAt""2019-07-30T19:33:27+08:00"
        }
    }

}

4

建議:

var Query = graphql.NewObject(graphql.ObjectConfig{}

func init(){
    // 資源一
    Query.AddFieldConfig("filedsName"&graphql.Field{})
}

func init(){
    // 資源二
}
var Mutation = graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
        "createVote"&graphql.Field{
            Type: vote.Vote,
            Args: graphql.FieldConfigArgument{
                "title"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "options"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
                },
                "description"&graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "deadline"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "class"&graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(vote.Class),
                },
            },
            Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
                log.Println(p.Args)
                var params vote.CreateVoteParams
                params.Title = p.Args["title"].(string)
                if p.Args["description"] != nil {
                    params.Description = p.Args["description"].(string)
                }
                params.Deadline = p.Args["deadline"].(string)
                params.Class = p.Args["class"].(int)
                var options []vote.OptionParams
                for _, i := range p.Args["options"].([]interface{}) {
                    var one vote.OptionParams
                    k := i.(map[string]interface{})
                    one.Name = k["name"].(string)
                    options = append(options, one)
                }
                params.Options = options
                log.Println(params)
                result, err := vote.CreateVote(params)
                if err != nil {
                    return nil, err
                }
                return result, nil

            },
        },
    },
})

Args 定義所有該請求的字段和類型。

p.Args 類型 (map[string]interface),可以獲取到請求參數。返回是個 interface, 根據 Args 內定義的類型,類型轉化

5

總結:本文簡單講解 GraphQL 的語法和 Go 編程實現 GraphQL 操作。

轉自:

zhuanlan.zhihu.com/p/75894765

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g2CNCdj3bIkMTgOEZwviUQ