Go GraphQL 教程
一般的 Web 開發都是使用 RESTful 風格進行 API 的開發,這種 RESTful 風格的 API 開發的一般流程是:
-
需求分析
-
模型設計
-
編碼實現
-
路由設計:
-
參數操作:校驗、請求
-
響應:JSON 格式、狀態碼
一種資源一般都可以抽象出 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 一般需要處理這些具體的問題:
-
根據需求進行模型設計:即 model 層,模型設計核心對應數據庫表,所以又需要根據需求,設計字段、字段類型、表的多對多等關係
-
抽象出資源實體,進行資源的增刪改查操作
-
返回 JSON 格式的響應、狀態碼、或者錯誤信息
前端或者客戶端,根據具體的需求,調用接口,對接口返回的字段進行處理。儘管有時候需求並不需要所有字段,又或者有時候需求需要 調用多個接口,組裝成一個大的格式,以完成需求。
後端抽象出多少實體,對應就會設計各種資源實體的接口。後續需求變更,爲了兼容,需要維護越來越多的接口。
看到沒,這類的接口設計:
-
需要維護多類接口,需求不斷變更,維護的接口越來越多
-
字段的獲取,前端或者客戶端調用者不能決定,而是一股腦的全部返回,再由相應開發人員處理
-
需要考慮接口版本
-
...
GraphQL API
GraphQL 是一種專門用於 API 的查詢語言,由大廠 Facebook 推出,但是至今 GraphQL 並沒有引起廣泛的使用, 絕大多少還是採用 RESTful API 風格的形式開發。
GraphQL 嘗試解決這些問題:
-
查詢語法和查詢結果高度相似
-
根據需求獲取字段
-
一個路由能獲取多個請求的結果
-
無需接口版本管理
1
既然是一種專門用於 API 的查詢語言,其必定有一些規範或者語法約束。具體 GraphQL 包含哪些知識呢?
- Schema 是類型語言的合集,定義了具體的操作 (比如:請求、更改),和對象信息(比如:響應的字段)
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
}
}
能發現一些規律麼?
-
schema 文件幾乎決定了請求的具體形式,請求什麼格式,響應什麼格式
-
API 請求動作包括:操作類型(query, mutation, subscription)、操作名稱、請求名稱、請求字段
query HeartBeat {
ping{
data
code
}
}
-
操作類型: query
-
操作名稱: HeartBeat (操作名稱一般省略)
-
請求名稱: ping
-
響應字段:Pong 對象的字段 data、code
GraphQL 是一種專門用於 API 的查詢語言,有語法約束。
具體包括:
-
別名:字段或者對象重命名、主要爲解決衝突問題
-
片段:簡單來說,就是提取公共字段,方便複用
-
變量:請求參數以變量的形式
-
指令:根據條件動態顯示字段:@include 是否包含該字段、@skip 是否不包含該字段、@deprecate 是否廢棄該字段
-
內聯片段:接口類型或者聯合類型中獲取下層字段
-
元字段
-
類型定義、對象定義
-
內置的類型:ID、Int、Float、String、Boolean, 其他類型使用基本類型構造對象類型即可
-
枚舉:可選值的集合
-
修飾符:
!
表示非空 -
接口:interface
-
聯合類型:
|
通過對象類型組合而成 -
輸入類型:爲解決傳遞複雜參數的問題
講了這麼些,其實最好的方式還是親自調用下接口,參照着官方文檔,按個調用嘗試下,熟悉這套語法規範。
最佳的當然是:Github 的 GraphQL API4 (https://developer.github.com/v4/)
-
熟絡 GraphQL 語法規範
-
學習 GraphQL 設計規範
登陸自己的賬號:訪問:https://developer.github.com/v4/explorer/
僅舉幾個示例:
0. viewer: User!
-
請求名稱:viewer
-
響應對象:User 非空,即一定會返回一個 User 對象,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年的生活過成一本書"
}
}
}
}
對照着文檔多嘗試。
-
https://developer.github.com/v4/explorer/
-
https://developer.github.com/v4/query/
上文多是講述使用 GraphQL 進行查詢操作時的語法。
2
schema 是所有請求、響應、對象聲明的集合,對後端而言,是開發依據,對前端而言,是 API 文檔。
如何定義 schema ?
你只需要知道這些內容即可:
-
內置的標量類型:ID(實質是字符串,唯一標識符)、Boolean、String、Float
-
修飾符
!
表示非空 -
對象類型:
type
關鍵字 -
枚舉類型:
enum
關鍵字 -
輸入類型:
input
關鍵字
舉一個具體的示例:小程序:騰訊投票
首頁
詳情
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
-
cmd: 命令行文件:主要用於同步數據庫表結構
-
main.go 函數主入口
-
model 模型定義,每種資源單獨一個文件 比如 vote.go
-
pkg 基礎設施:數據庫連接、路由設計
-
web 核心業務路徑,總體上按資源劃分文件夾
-
vote
-
vote_curd.go 資源的增刪改查
-
vote_params.go 請求參數
-
vote_type.go schema 中資源,即類型對象的定義
-
query
-
query.go
-
mutation
-
mutation.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,
}
}
依然保持了個人的模型設計風格:
-
定義一個結構體,對應數據庫表
-
定義個序列化結構體,對應模型的響應
-
單選、多選項,實質在數據庫中用 0,1 表示,響應顯示中文:單選、多選
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
}
-
Fields 表示對象字段
-
Type 表示返回類型
-
Args 表示參數
-
Resolve 表示具體的處理函數
內置類型:(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 客戶端,絕大多數工作都在定義對象、定義字段類型、定義字段的處理函數等。
-
graphql.Object
-
graphql.InputObject
-
graphql.Enum
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: 運行,接口調用
-
只有一個路由:
/graphql
-
無需版本管理
-
所有的請求方法都是:
POST
(query 動作當然也可以使用 GET,遇到請求參數較多時,不方便)
接口調用示例:(根據查詢文檔,可以根據調用者的需求,自主選擇響應的字段)
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
建議:
-
優先設計:Schema, 指導着開發者
-
如果請求或者更改動作過多,按功能或者資源劃分(項目結構按功能劃分,一定程度上有助於減輕思維負擔)
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