項(xiàng)目實(shí)戰(zhàn):使用 Go 構(gòu)建 GraphQL API

2020/5/16 更新:大家好,我剛剛更新了該項(xiàng)目以使用 Go module。不幸的是,realize[1]很長(zhǎng)時(shí)間沒(méi)有更新并且無(wú)法正常工作。如果您想使用實(shí)時(shí)重新加載器,則還有其他選擇,例如 air[2]。否則,請(qǐng)隨意忽略帖子中有關(guān) realize 的任何內(nèi)容,并按通常的方式運(yùn)行項(xiàng)目。
本博文中將使用 Go、GraphQL、PostgreSQL 創(chuàng)建一個(gè) API。我已在項(xiàng)目結(jié)構(gòu)上迭代幾個(gè)版本,這是我最喜歡的一個(gè)。在大部分的時(shí)間,我創(chuàng)建 web APIs 都是通過(guò) Node.js 和 Ruby/Rails。而第一次使用 Go 設(shè)計(jì) Web apis 時(shí),需要費(fèi)很大的勁兒。Ben Johnson 的 Structuring Applications in Go[3] 文章對(duì)我有很大的幫助,本博文中的部分代碼就得益于 Ben Johnson 文章的指導(dǎo),推薦閱讀。
配置
首先,從項(xiàng)目的配置開(kāi)始。在本篇博文中,我將在 macOS 中完成,但這并不重要。如果在你的 macOS 上還沒(méi)有 Go 和 PostGreSQL,bradford-hamilton/go-graphql-api[4] 詳細(xì)講解了如何在 macOS 上配置 Go 和 PostgreSQL.
創(chuàng)建一個(gè)新項(xiàng)目--go-graphal-api,整體項(xiàng)目結(jié)構(gòu)如下:
├──?gql
│???├──?gql.go
│???├──?queries.go
│???├──?resolvers.go
│???└──?types.go
├──?main.go
├──?postgres
│???└──?postgres.go
└──?server
?└──?server.go
有一些額外依賴需要安裝。開(kāi)發(fā)中熱加載的 realize[5],go-chi 的輕量級(jí)路由 chi[6] 和管理 request/response 負(fù)載的 render[7],以及 graphql-go/graphql[8]。
go?get?github.com/oxequa/realize
go?get?github.com/go-chi/chi
go?get?github.com/go-chi/render
go?get?github.com/graphql-go/graphql
go?get?github.com/lib/pq
最后,創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)和一些測(cè)試使用的數(shù)據(jù),在 Postgres 的命令行中輸入 psql,創(chuàng)建一個(gè)數(shù)據(jù)庫(kù):
CREATE?DATABASE?go_graphql_db;
然后連接上該庫(kù):
\c?go_graphql_db
連接上后,將以下 sql 語(yǔ)句粘貼到命令行:
CREATE?TABLE?users?(
??id?serial?PRIMARY?KEY,
??name?VARCHAR?(50)?NOT?NULL,
??age?INT?NOT?NULL,
??profession?VARCHAR?(50)?NOT?NULL,
??friendly?BOOLEAN?NOT?NULL
);
INSERT?INTO?users?VALUES
??(1,?'kevin',?35,?'waiter',?true),
??(2,?'angela',?21,?'concierge',?true),
??(3,?'alex',?26,?'zoo?keeper',?false),
??(4,?'becky',?67,?'retired',?false),
??(5,?'kevin',?15,?'in?school',?true),
??(6,?'frankie',?45,?'teller',?true);
我們創(chuàng)建了一個(gè)基礎(chǔ)的用戶表并新增了 6 條新用戶數(shù)據(jù),對(duì)本博文來(lái)說(shuō)已經(jīng)足夠。接下來(lái)開(kāi)始構(gòu)建我們的 API。
API
在這篇博文中,所有的代碼片段都會(huì)包含一些注釋,以幫助理解每一步。
從 main.go 開(kāi)始:
package?main
import?(
?"fmt"
?"log"
?"net/http"
?"github.com/bradford-hamilton/go-graphql-api/gql"
?"github.com/bradford-hamilton/go-graphql-api/postgres"
?"github.com/bradford-hamilton/go-graphql-api/server"
?"github.com/go-chi/chi"
?"github.com/go-chi/chi/middleware"
?"github.com/go-chi/render"
?"github.com/graphql-go/graphql"
)
func?main()?{
?//?Initialize?our?API?and?return?a?pointer?to?our?router?for?http.ListenAndServe
?//?and?a?pointer?to?our?db?to?defer?its?closing?when?main()?is?finished
?router,?db?:=?initializeAPI()
?defer?db.Close()
?//?Listen?on?port?4000?and?if?there's?an?error?log?it?and?exit
?log.Fatal(http.ListenAndServe(":4000",?router))
}
func?initializeAPI()?(*chi.Mux,?*postgres.Db)?{
?//?Create?a?new?router
?router?:=?chi.NewRouter()
?//?Create?a?new?connection?to?our?pg?database
?db,?err?:=?postgres.New(
??postgres.ConnString("localhost",?5432,?"bradford",?"go_graphql_db"),
?)
?if?err?!=?nil?{
??log.Fatal(err)
?}
?//?Create?our?root?query?for?graphql
?rootQuery?:=?gql.NewRoot(db)
?//?Create?a?new?graphql?schema,?passing?in?the?the?root?query
?sc,?err?:=?graphql.NewSchema(
??graphql.SchemaConfig{Query:?rootQuery.Query},
?)
?if?err?!=?nil?{
??fmt.Println("Error?creating?schema:?",?err)
?}
?//?Create?a?server?struct?that?holds?a?pointer?to?our?database?as?well
?//?as?the?address?of?our?graphql?schema
?s?:=?server.Server{
??GqlSchema:?&sc,
?}
?//?Add?some?middleware?to?our?router
?router.Use(
??render.SetContentType(render.ContentTypeJSON),?//?set?content-type?headers?as?application/json
??middleware.Logger,??????????//?log?API?request?calls
??middleware.DefaultCompress,?//?compress?results,?mostly?gzipping?assets?and?json
??middleware.StripSlashes,????//?match?paths?with?a?trailing?slash,?strip?it,?and?continue?routing?through?the?mux
??middleware.Recoverer,???????//?recover?from?panics?without?crashing?server
?)
?//?Create?the?graphql?route?with?a?Server?method?to?handle?it
?router.Post("/graphql",?s.GraphQL())
?return?router,?db
}
在上面導(dǎo)入的 gql、postgres 和 server 的路徑應(yīng)該是你本地的路徑,以及 postgres.ConnString() 中連接 PostgreSQL 的用戶名也應(yīng)該是你自己的,和我的不一樣。
initializeAPI() 分為幾大塊主要的部分,接下來(lái)我們逐步構(gòu)建每一塊。
使用 chi.NewRouter() 創(chuàng)建 router 并返回一個(gè) mux,接下來(lái)是創(chuàng)建一個(gè) PostgreSQL 數(shù)據(jù)庫(kù)連接。
使用 postgres.ConnString() 創(chuàng)建一個(gè) string 類型的連接配置,并封裝到 postgres.New() 函數(shù)中。這些邏輯在我們自己包中的 postgres.go 文件中構(gòu)建:
package?postgres
import?(
?"database/sql"
?"fmt"
?//?postgres?driver
?_?"github.com/lib/pq"
)
//?Db?is?our?database?struct?used?for?interacting?with?the?database
type?Db?struct?{
?*sql.DB
}
//?New?makes?a?new?database?using?the?connection?string?and
//?returns?it,?otherwise?returns?the?error
func?New(connString?string)?(*Db,?error)?{
?db,?err?:=?sql.Open("postgres",?connString)
?if?err?!=?nil?{
??return?nil,?err
?}
?//?Check?that?our?connection?is?good
?err?=?db.Ping()
?if?err?!=?nil?{
??return?nil,?err
?}
?return?&Db{db},?nil
}
//?ConnString?returns?a?connection?string?based?on?the?parameters?it's?given
//?This?would?normally?also?contain?the?password,?however?we're?not?using?one
func?ConnString(host?string,?port?int,?user?string,?dbName?string)?string?{
?return?fmt.Sprintf(
??"host=%s?port=%d?user=%s?dbname=%s?sslmode=disable",
??host,?port,?user,?dbName,
?)
}
//?User?shape
type?User?struct?{
?ID?????????int
?Name???????string
?Age????????int
?Profession?string
?Friendly???bool
}
//?GetUsersByName?is?called?within?our?user?query?for?graphql
func?(d?*Db)?GetUsersByName(name?string)?[]User?{
?//?Prepare?query,?takes?a?name?argument,?protects?from?sql?injection
?stmt,?err?:=?d.Prepare("SELECT?*?FROM?users?WHERE?name=$1")
?if?err?!=?nil?{
??fmt.Println("GetUserByName?Preperation?Err:?",?err)
?}
?//?Make?query?with?our?stmt,?passing?in?name?argument
?rows,?err?:=?stmt.Query(name)
?if?err?!=?nil?{
??fmt.Println("GetUserByName?Query?Err:?",?err)
?}
?//?Create?User?struct?for?holding?each?row's?data
?var?r?User
?//?Create?slice?of?Users?for?our?response
?users?:=?[]User{}
?//?Copy?the?columns?from?row?into?the?values?pointed?at?by?r?(User)
?for?rows.Next()?{
??err?=?rows.Scan(
???&r.ID,
???&r.Name,
???&r.Age,
???&r.Profession,
???&r.Friendly,
??)
??if?err?!=?nil?{
???fmt.Println("Error?scanning?rows:?",?err)
??}
??users?=?append(users,?r)
?}
?return?users
}
上面的思想是:創(chuàng)建數(shù)據(jù)庫(kù)的連接并返回持有該連接的Db對(duì)象。然后創(chuàng)建了一個(gè) db 的 GetUserByUsername() 方法。
將關(guān)注點(diǎn)重新回到 main.go 文件,在 40 行處創(chuàng)建了一個(gè) root query 用于構(gòu)建 GraphQL 的 schema。我們?cè)?gql 包下的 queries.go 中創(chuàng)建:
package?gql
import?(
?"github.com/bradford-hamilton/go-graphql-api/postgres"
?"github.com/graphql-go/graphql"
)
//?Root?holds?a?pointer?to?a?graphql?object
type?Root?struct?{
?Query?*graphql.Object
}
//?NewRoot?returns?base?query?type.?This?is?where?we?add?all?the?base?queries
func?NewRoot(db?*postgres.Db)?*Root?{
?//?Create?a?resolver?holding?our?databse.?Resolver?can?be?found?in?resolvers.go
?resolver?:=?Resolver{db:?db}
?//?Create?a?new?Root?that?describes?our?base?query?set?up.?In?this
?//?example?we?have?a?user?query?that?takes?one?argument?called?name
?root?:=?Root{
??Query:?graphql.NewObject(
???graphql.ObjectConfig{
????Name:?"Query",
????Fields:?graphql.Fields{
?????"users":?&graphql.Field{
??????//?Slice?of?User?type?which?can?be?found?in?types.go
??????Type:?graphql.NewList(User),
??????Args:?graphql.FieldConfigArgument{
???????"name":?&graphql.ArgumentConfig{
????????Type:?graphql.String,
???????},
??????},
??????Resolve:?resolver.UserResolver,
?????},
????},
???},
??),
?}
?return?&root
}
在 NewRoot() 方法中傳入 db,并使用該 db 創(chuàng)建一個(gè) Resolver。在Resolver方法中對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作。
然后創(chuàng)建了一個(gè) new root 用于用戶的查詢,需要name作為查詢參數(shù)。類型是 graphql.NewList 的 User(切片或者數(shù)組類型),在 gql 包下的 type.go 文件中定義。如果有其他類型的查詢,就在這個(gè) root 中增加。要把引入的 postgres 包改成自己本地的包。
接下來(lái)看一下 resolvers.go:
package?gql
import?(
?"github.com/bradford-hamilton/go-graphql-api/postgres"
?"github.com/graphql-go/graphql"
)
//?Resolver?struct?holds?a?connection?to?our?database
type?Resolver?struct?{
?db?*postgres.Db
}
//?UserResolver?resolves?our?user?query?through?a?db?call?to?GetUserByName
func?(r?*Resolver)?UserResolver(p?graphql.ResolveParams)?(interface{},?error)?{
?//?Strip?the?name?from?arguments?and?assert?that?it's?a?string
?name,?ok?:=?p.Args["name"].(string)
?if?ok?{
??users?:=?r.db.GetUsersByName(name)
??return?users,?nil
?}
?return?nil,?nil
}
這里導(dǎo)入的 postgres 包同樣是你本地的。在這個(gè)地方還可以增加其他需要的解析器。
接下來(lái)看 types.go:
package?gql
import?"github.com/graphql-go/graphql"
//?User?describes?a?graphql?object?containing?a?User
var?User?=?graphql.NewObject(
?graphql.ObjectConfig{
??Name:?"User",
??Fields:?graphql.Fields{
???"id":?&graphql.Field{
????Type:?graphql.Int,
???},
???"name":?&graphql.Field{
????Type:?graphql.String,
???},
???"age":?&graphql.Field{
????Type:?graphql.Int,
???},
???"profession":?&graphql.Field{
????Type:?graphql.String,
???},
???"friendly":?&graphql.Field{
????Type:?graphql.Boolean,
???},
??},
?},
)
類似的,在這里添加我們不同的類型,每一個(gè)字段都指定了類型。在 main.go 文件的 42 行使用 root query 創(chuàng)建了一個(gè)新的查詢。
差不多好了
在 main.go 往下的 51 行處,創(chuàng)建一個(gè)新的 server,server 持有 GraphQL schema 的指針。下面是 server.go 的內(nèi)容:
package?server
import?(
?"encoding/json"
?"net/http"
?"github.com/bradford-hamilton/go-graphql-api/gql"
?"github.com/go-chi/render"
?"github.com/graphql-go/graphql"
)
//?Server?will?hold?connection?to?the?db?as?well?as?handlers
type?Server?struct?{
?GqlSchema?*graphql.Schema
}
type?reqBody?struct?{
?Query?string?`json:"query"`
}
//?GraphQL?returns?an?http.HandlerFunc?for?our?/graphql?endpoint
func?(s?*Server)?GraphQL()?http.HandlerFunc?{
?return?func(w?http.ResponseWriter,?r?*http.Request)?{
??//?Check?to?ensure?query?was?provided?in?the?request?body
??if?r.Body?==?nil?{
???http.Error(w,?"Must?provide?graphql?query?in?request?body",?400)
???return
??}
??var?rBody?reqBody
??//?Decode?the?request?body?into?rBody
??err?:=?json.NewDecoder(r.Body).Decode(&rBody)
??if?err?!=?nil?{
???http.Error(w,?"Error?parsing?JSON?request?body",?400)
??}
??//?Execute?graphql?query
??result?:=?gql.ExecuteQuery(rBody.Query,?*s.GqlSchema)
??//?render.JSON?comes?from?the?chi/render?package?and?handles
??//?marshalling?to?json,?automatically?escaping?HTML?and?setting
??//?the?Content-Type?as?application/json.
??render.JSON(w,?r,?result)
?}
}
在 server 中有一個(gè) GraphQL 的方法,這個(gè)方法的主要作用就是處理 GraphQL 的查詢。記得將 gql 的路徑更新為你本地的路徑。
接下來(lái)看最后一個(gè)文件 gql.go:
package?gql
import?(
?"fmt"
?"github.com/graphql-go/graphql"
)
//?ExecuteQuery?runs?our?graphql?queries
func?ExecuteQuery(query?string,?schema?graphql.Schema)?*graphql.Result?{
?result?:=?graphql.Do(graphql.Params{
??Schema:????????schema,
??RequestString:?query,
?})
?//?Error?check
?if?len(result.Errors)?>?0?{
??fmt.Printf("Unexpected?errors?inside?ExecuteQuery:?%v",?result.Errors)
?}
?return?result
}
這里只有一個(gè)簡(jiǎn)單的 ExecuteQuery() 函數(shù)用來(lái)執(zhí)行 GraphQL 查詢。在這里可能會(huì)有一個(gè)類似于 ExecuteMutation() 函數(shù)用來(lái)處理 GraphQL 的 mutations。
在 initializeAPI() 的最后,在 router 中增加一些中間工具,以及增加處理 /graphql POSTs 請(qǐng)求的 GraphQL server 方法。并且在這個(gè)地方增加其他 RESTful 請(qǐng)求的路由,并在 server 中增加處理路由的方法。
然后在項(xiàng)目的根目錄運(yùn)行 realize init,會(huì)有兩次提示信息并且兩次都輸入 n

下面是在你項(xiàng)目的根目錄下創(chuàng)建的 .realize.yaml 文件:
settings:
??legacy:
?force:?false
?interval:?0s
schema:
-?name:?go-graphql-api
??path:?.
??commands:
?run:
???status:?true
??watcher:
?extensions:
?-?go
?paths:
?-?/
?ignored_paths:
?-?.git
?-?.realize
?-?vendor
這段配置對(duì)于監(jiān)控你項(xiàng)目里面的改變非常重要,如果檢測(cè)到有改變,將自動(dòng)重啟 server 并重新運(yùn)行 main.go 文件。
有一些開(kāi)發(fā) GraphQL API 非常好的工具,比如:graphiql[9]、insomnia[10]、graphql-playground[11],還可以發(fā)送一個(gè) application/json 請(qǐng)求體的 POST 請(qǐng)求,比如:
{
?"query":?"{users(name:\"kevin\"){id,?name,?age}}"
}
在 Postman[12] 里像下面這樣:

在查詢中可以只請(qǐng)求一個(gè)屬性或者多個(gè)屬性的組合。在 GraphQL 的正式版中,可以只請(qǐng)求我們希望通過(guò)網(wǎng)絡(luò)發(fā)送的信息。
很成功
大功告成!希望這篇博文對(duì)你在 Go 中編寫(xiě) GraphQL API 有幫助。我嘗試將功能分解到不同的包或文件中,使其更容易擴(kuò)展,而且每一塊也很容易測(cè)試。
via: https://medium.com/@bradford_hamilton/building-an-api-with-graphql-and-go-9350df5c9356
作者:Bradford Lamson-Scribner[13]譯者:HelloJavaWorld123[14]校對(duì):polaris1119[15]
本文由 GCTT[16] 原創(chuàng)編譯,Go 中文網(wǎng)[17] 榮譽(yù)推出
參考資料
realize: https://github.com/oxequa/realize
[2]air: https://github.com/cosmtrek/air
[3]Structuring Applications in Go: https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091
[4]bradford-hamilton/go-graphql-api: https://github.com/github.com/bradford-hamilton/go-graphql-api
[5]realize: https://github.com/oxequa/realize
[6]chi: https://github.com/go-chi/chi
[7]render: https://github.com/go-chi/render
[8]graphql-go/graphql: https://github.com/graphql-go/graphql
[9]graphiql: https://github.com/graphql/graphiql
[10]insomnia: https://insomnia.rest/
[11]graphql-playground: https://github.com/prisma/graphql-playground
[12]Postman: https://www.getpostman.com/
[13]Bradford Lamson-Scribner: https://medium.com/@bradford_hamilton
[14]HelloJavaWorld123: https://github.com/HelloJavaWorld123
[15]polaris1119: https://github.com/polaris1119
[16]GCTT: https://github.com/studygolang/GCTT
[17]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
