Go中的GraphQL vs REST:双雄对决,如何避免选错
\n\n如果您使用Go语言至少半年,您很可能遇到过这个永恒的争论:REST还是GraphQL?一个是久经考验的“老黄牛”,另一个是功能丰富的时尚“跑车”。但在Go的世界里,性能和简洁是信条,选择并不总是那么显而易见。
\n\n在这篇文章中,我们不仅仅列举优缺点。我们将编写真实的Go代码,测量执行时间,并分析何时GraphQL是搬起石头砸自己的脚,何时又是通往高效API世界的捷径。开始吧!
\n\nGo中的REST:稳定与可预测
\n\nREST就像一把久经考验的旧锤子。它不耀眼,但钉钉子非常完美。在Go中,REST API使用net/http或像gin和chi这样的轻量级框架编写。让我们看一个简单的例子:
package main\n\nimport (\n "encoding/json"\n "net/http"\n)\n\ntype User struct {\n ID int `json:"id"`\n Name string `json:"name"`\n}\n\nfunc getUserHandler(w http.ResponseWriter, r *http.Request) {\n user := User{ID: 1, Name: "Alexey"}\n w.Header().Set("Content-Type", "application/json")\n json.NewEncoder(w).Encode(user)\n}\n\nfunc main() {\n http.HandleFunc("/api/user", getUserHandler)\n http.ListenAndServe(":8080", nil)\n}\n\n漂亮吗?是的。简单吗?非常。但是,如果前端只需要用户名,而我们却拉取了整个对象呢?在REST中,这被称为过度获取——数据冗余。在Go中,我们习惯于控制每一个字节,这可能会让人抓狂。
\n\nREST中的N+1问题
\n\n假设我们有一个订单列表,并且需要每个订单的用户名。在REST中,我们要么发出N个请求(慢),要么创建一个单独的端点/api/orders-with-users(端点过多)。Go通过在数据库中使用join查询来处理这个问题,但在API层面,这看起来仍然很笨拙。
Go中的GraphQL:强大与灵活
\n\nGraphQL就像一把瑞士军刀:一个端点,客户端自己决定需要哪些数据。在Go中,有gqlgen框架(来自99designs)——它根据模式生成代码,这非常符合Go的风格(更少的样板代码,更多的工作)。
// schema.graphql\ntype Query {\n user(id: Int!): User\n}\n\ntype User {\n id: Int!\n name: String!\n orders: [Order!]!\n}\n\ntype Order {\n id: Int!\n total: Float!\n}\n\n现在是在Go中的实现:
\n\npackage graph\n\nimport (\n "context"\n "fmt"\n)\n\ntype Resolver struct{}\n\nfunc (r *Resolver) User(ctx context.Context, args struct{ ID int }) (*User, error) {\n // 实际应用中——查询数据库\n return &User{\n ID: args.ID,\n Name: "Maria",\n Orders: []*Order{\n {ID: 1, Total: 250.50},\n },\n }, nil\n}\n\n客户端只请求name——就只得到name。没有过度获取。但有一个问题:如果客户端请求10个用户的orders,并且每个用户有50个订单——数据库可能会崩溃。这在GraphQL中被称为N+1问题,在Go中通过DataLoaders来解决。
DataLoader——N+1问题的救星
\n\nimport "github.com/graph-gophers/dataloader"\n\ntype UserLoader struct {\n *dataloader.Loader\n}\n\nfunc NewUserLoader() *UserLoader {\n return &UserLoader{\n Loader: dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n // 批量查询数据库\n userIDs := make([]int, len(keys))\n for i, key := range keys {\n userIDs[i] = key.Int()\n }\n users := batchFetchUsers(userIDs) // 一个带有WHERE IN的SQL查询\n results := make([]*dataloader.Result, len(ke