Go GraphQL 教程
大家好,我叫谢伟,是一名程序员。
今天的主题:Go GraphQL 教程。
RESTful API 设计
一般的 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年的生活过成一本书"
}
}
}
}
对照着文档多尝试。
上文多是讲述使用 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
- 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
(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 操作。
建议如何学习?
- 中文官网:https://graphql.cn/
- 配合 github api 调用,熟悉语法
- 示例1: https://github.com/topliceanu/graphql-go-example
- 示例2: https://github.com/wuxiaoxiaoshen/Gopher-By-Example/tree/master/graphql-example
<完>