Go GraphQL 教程

Go GraphQL 教程

你們好,我叫謝偉,是一名程序員。前端

今天的主題:Go GraphQL 教程。git

RESTful API 設計

通常的 Web 開發都是使用 RESTful 風格進行API的開發,這種 RESTful 風格的 API 開發的通常流程是:程序員

  • 需求分析
  • 模型設計
  • 編碼實現
    • 路由設計:
    • 參數操做:校驗、請求
    • 響應:JSON 格式、狀態碼

一種資源通常均可以抽象出 4 類路由,好比投票接口:github

# 獲取全部投票信息
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}

複製代碼

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

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

設計這類 API 通常須要處理這些具體的問題:編程

  • 根據需求進行模型設計:即 model 層,模型設計核心對應數據庫表,因此又須要根據需求,設計字段、字段類型、表的多對多等關係
  • 抽象出資源實體,進行資源的增刪改查操做
  • 返回JSON 格式的響應、狀態碼、或者錯誤信息

前端或者客戶端,根據具體的需求,調用接口,對接口返回的字段進行處理。儘管有時候需求並不須要全部字段,又或者有時候需求須要 調用多個接口,組裝成一個大的格式,以完成需求。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 (developer.github.com/v4/)

  • 熟絡 GraphQL 語法規範
  • 學習 GraphQL 設計規範

登入本身的帳號:訪問: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年的生活過成一本書"
      }
    }
  }
}

複製代碼

對照着文檔多嘗試。

上文可能是講述使用 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 操做。

建議如何學習?

<完>

相關文章
相關標籤/搜索