距離開始用 Go 開發後端已經有一段時間了,做了很多大大小小的專案。一開始是用 Go 社群人氣相對較高的 GIN 作為後端框架,開發 RESTful 的 API,但後來在接觸並瞭解了 GraphQL 以後,發現這個東西和我對 API 設計的想法好像更近,所以開始找 Go 後端開發有沒有什麼 GraphQL 的框架,然後找到了 gqlgen 這個框架,他和以往的 RESTful 後端開發模式有著很大的差異,因此想寫這篇文章分享他的不同之處,以及他是如何為後端開發帶來便利的。
什麼是 GraphQL
為了避免閱讀本文的讀者還沒聽過 GraphQL ,或是沒有深入瞭解過,這邊簡短的對 GraphQL 做一個介紹。
在傳統的 HTTP 通訊協定中,有 GET, POST, DELETE, PUT ... 等這種所謂的方法 (Method),而我們通過 方法 + 路徑 的方式組合出了一個前端想要告訴後端的查詢或操作,例如:
GET /users
代表我們想要 取得 所有使用者的資料。
PUT /users/ken20001207
代表我們想要 修改 使用者中,一個名叫 ken20001207 的使用者。
除了方法和路徑,我們還可以用參數 (query params) 來對查詢做出一些補充,例如:
GET /users?limt=10
代表我想要查詢使用者的資料,但最多 回傳 10 筆資料 就好。
這樣的設計看似很完美,不僅功能完整且很直觀,學習成本很低。但如果我們考慮一些實際的應用場景,比如說每個 User 是長這樣的:
type User { id: ID name: String! bio: String! email: String! friends: [User!]! }
並且給出這樣的假設: id, name, bio, email 是直接儲存在資料庫中的資料,但 bio 資料的內容可能很長很長。 friends 這個欄位不是儲存在資料庫中的,而是需要透過 SQL 的 JOIN 或 MongoDB 的 lookup 來進階查詢的。
這個時候就遇到一個問題,在 GET /users 我回傳的 User 需要包含 bio 和 friends 這兩個欄位嗎?如果包含的話,後端需要花更多的時間幫我查詢,而且 response 的大小會變的更大。
那麼我們換個想法,讓前端在請求的時候可以自己決定需要哪些欄位,如何?比如說我在 GET 的時候加入這個參數:
GET /users?field=id&field=name&field=bio&field=friends
用 field=xxx 告訴後端我需要哪些欄位,不需要的就不用幫我查了。嗯 ... 這看似可行,但 friends 這個欄位裡面的類型也是 User 呀!那這些 friends 欄位內的 User 又需要回傳哪些欄位呢?
GraphQL 的設計幫我們解決了這個問題。首先,我們直接不管原本的 GET, POST, PUT ... 那些 method 了,只有 Query 和 Mutation 兩種:Query 代表我想要和後端 查詢 一些東西, Mutation 代表我想要讓後端 執行 一些操作。例如:
query { users { id name } }
這樣就代表我想要查詢使用者的資料,但只需要 id 和 name 這兩個欄位。
query { users { id friends { id name } } }
這樣就代表我想要查詢使用者的資料,但只需要 id 和 friends 這兩個欄位,而 friends 裡面的 User 只需要給我 id 和 name 就好。
query { users(limit: 10) { id friends(limit: 5) { id name } } }
參數也沒問題,而且更加強大!是不是很方便呢?
因為篇幅的關係就不再繼續深入介紹 GraphQL 的基本概念和語法,有興趣的讀者可以去 GraphQL 的官方介紹 瞭解其他更多的特性。
什麼是 gqlgen

知道什麼是 GraphQL 以後,我們來瞭解一下 gqlgen 這個框架,他可以幫我們用一種方便快速的方式實現一個 Go 的 GraphQL API 後端。
每一個 GraphQL API 都會有一份自己的 GraphQL Schema,用 GraphQL 語法編寫而成,裡面定義了所有這個 API 的類型以及 Query 和 Mutation 的接口定義。
type Query { users: [User!]! } type Mutation { updateUserEmail(id: ID!, email: String!): User } type User { id: ID! name: String! email: String! friends: [User!]! }
我們必須先定義好這個 Schema,然後才開始根據他的定義進行開發,也就是所謂的 Schema-Driven Development。
在使用 gqlgen 的時候,這是一個非常重要的步驟。因為 gqlgen 會讀取你的 Schema 定義,然後 自動生成 根據你這份 Schema 設計定義出來的一套程式碼模板,我們只要在模板內實作對應的邏輯即可。這個 自動生成 的步驟也是為什麼框架名叫 gqlgen 的原因。
舉例而言,對於上面的範例 Schema,可以生成出這樣的模板:
func (r *userResolvers) Users(ctx Context) ([]*User, error) { // 在這裡實作如何從資料庫獲取使用者資料的邏輯 return /* ... */ } func (r *userResolvers) UpdateUserEmail(ctx Context, id string, email string) (*User, error) { // 在這裡實作如何更新使用者 Email 的邏輯 return /* ... */ }
當我們完成這個模板內的 resolver 邏輯實作以後,gqlgen 就可以自動地幫我們根據前端傳入的 Request 來呼叫不同的 resolver,然後把 resolver 執行完畢回傳的的資料組合起來,再傳送回去。
這時候讀者應該想到了一個問題:我們並沒有解決 User 的 friends 欄位需不需要回傳的問題呀,即使前端用 GraphQL 發來請求說不需要 friends,我在 userResolver 裡面不是仍然計算並回傳了嗎?
這就是我覺得 gqlgen 這個框架設計的很精妙的地方了。
從上面的範例來說,Go 程式碼的 User 這個 type 實際上是 gqlgen 幫我們根據 Schema 生成的。然而,如同上面所說,這個 User 類型其實所有的欄位分為兩種:直接存在資料庫的、需要額外計算的。
我們可以 不使用 gqlgen 自動生成的 type 定義,自己在 Go 程式碼寫一個 User type 定義。當 gqlgen 發現你自己寫了一個 type,他就會改用你定義的這個 type。
但我們自己在 Go 裡面定義的 User type 是這樣的:
type User struct { ID string Name string Email string }
這個時候 gqlgen 會發現,你定義的這個 type 和 schema 定義的 User 不一樣!明明 Schema 有 friends 這個欄位,但這個 type 裡面卻沒有 ... 所以他生成的程式模板就會要求你補上這個 User 的 friends 欄位該如何 resolve:
func (r *userResolvers) Users(ctx Context) ([]*User, error) { // 在這裡實作如何從資料庫獲取使用者資料的邏輯 // 但回傳的 User 不需要包含 friends 欄位! return /* ... */ } func (r *userResolvers) Friends(ctx Context, obj *User) ([]*User, error) { // 在這裡實作如何「獲取某個 User 的 friends」的邏輯 return /* ... */ }
這時候他就幫你把 「可以直接從資料庫拿到的欄位」 和 「需要額外計算」 的欄位切開來了,我們可以分開實現這兩個邏輯,然後 gqlgen 會根據前端的 query 請求自動呼叫需要的 resolver,大大降低了後端開發時對於這種複雜 Schema 定義在實現上的成本。
不會有效能問題嗎?
大家看完上面的介紹,想必已經迫不及待的要提出質疑了:
是的,這也是我一開始聽到這個方法的第一個反應,這個作法想必會造成嚴重的效能問題!然而沒想到 gqlgen 早已有準備,在官方文檔給出了解釋:
Optimizing N+1 database queries using Dataloaders | gqlgen
官方給出的解決方案是用 Dataloader 的概念來解決這個問題,有興趣的同學建議點進去上面的網址看看官方的舉例,我這裡只是簡單的做個說明。
簡單來說就是,對於你這 N 個資料庫的 Query,Dataloader 會 ...
- 把重複的部分聚合在一起(查一次就好,然後回傳一樣的內容回去給所有人)
- 聚合在一起以後,再把相同的類型聚合在一起(查一次就好,再根據你要什麼資料,從查詢結果挑出你要的部分)
經過這樣兩次聚合,實際上 N+1 問題已經被做到很大程度的優化了。我們甚至可以在 Dataloader 層加入 Cache 邏輯,再更進一步的優化效能。
結語
今天這篇文主要是想推薦 GraphQL 和 gqlgen 給大家,雖然自己也還沒有更深入的瞭解其他功能和原理,但單純就一個框架使用者的角度,這個 Workflow 是自己近期很滿意的一套解決方案。
也希望閱讀這篇文章的各位大大如果不同的觀點,可以來 Chief Noob 開發社群 | Discord Server 和我們一起分享與討論 👍