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 的查询语言,有语法约束。
具体包括:
! |
讲了这么些,其实最好的方式还是亲自调用下接口,参照着官方文档,按个调用尝试下,熟悉这套语法规范。
最佳的当然是: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 ?
你只需要知道这些内容即可:
! 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
- vote
和之前的 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
接口调用示例:(根据查询文档,可以根据调用者的需求,自主选择响应的字段)
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 内定义的类型,类型转化