你們好,我叫謝偉,是一名程序員。前端
今天的主題:Go GraphQL 教程。git
通常的 Web 開發都是使用 RESTful 風格進行API的開發,這種 RESTful 風格的 API 開發的通常流程是:程序員
一種資源通常均可以抽象出 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 通常須要處理這些具體的問題:編程
前端或者客戶端,根據具體的需求,調用接口,對接口返回的字段進行處理。儘管有時候需求並不須要全部字段,又或者有時候需求須要 調用多個接口,組裝成一個大的格式,以完成需求。json
後端抽象出多少實體,對應就會設計各類資源實體的接口。後續需求變動,爲了兼容,須要維護愈來愈多的接口。小程序
看到沒,這類的接口設計:後端
GraphQL 是一種專門用於API 的查詢語言,由大廠 Facebook 推出,可是至今 GraphQL 並無引發普遍的使用, 絕大多少仍是採用 RESTful API 風格的形式開發。
GraphQL 嘗試解決這些問題:
既然是一種專門用於 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 (developer.github.com/v4/)
登入本身的帳號:訪問: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 進行查詢操做時的語法。
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文檔。
客戶端如何使用: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: 運行,接口調用
/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"
}
}
}
複製代碼
建議:
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 內定義的類型,類型轉化
總結:本文簡單講解 GraphQL的語法和 Go 編程實現 GraphQL 操做。
建議如何學習?
<完>