 
                    背景
2022 年 11 月正式掌管 Twitter 的马斯克发推批判 Twitter 开发团队:Twitter 因批量执行 RPC 调用,导致非美国地区的用户访问延迟较高。
新闻源地址:https://view.inews.qq.com/a/20221114A041F400
那么,究竟孰是孰非?下面是作者整理的架构图。
GraphQL
请求你所要的数据不多不少
向你的 API 发出一个 GraphQL 请求就能准确获得你想要的数据,不多不少。GraphQL 查询总是返回可预测的结果。使用 GraphQL 的应用可以工作得又快又稳,因为控制数据的是应用,而不是服务器。
获取多个资源只用一个请求
GraphQL 查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询。典型的 REST API 请求多个资源时得载入多个 URL,而 GraphQL 可以通过一次请求就获取你应用所需的所有数据。这样一来,即使是比较慢的移动网络连接下,使用 GraphQL 的应用也能表现得足够迅速。
描述所有的可能类型系统
GraphQL API 基于类型和字段的方式进行组织,而非入口端点。你可以通过一个单一入口端点得到你所有的数据能力。GraphQL 使用类型来保证应用只请求可能的数据,还提供清晰的辅助性错误信息。应用可以使用类型,而避免编写手动解析代码。
快步前进,强大的开发者工具
不用离开编辑器就能准确知道你可以从 API 中请求的数据,发送查询之前就能高亮潜在问题,高级代码智能提示。利用 API 的类型系统,GraphQL 让你可以更简单地构建如同 GraphiQL (https://github.com/graphql/graphiql)的强大工具。
API 演进无需划分版本
给你的 GraphQL API 添加字段和类型而无需影响现有查询。老旧的字段可以废弃,从工具中隐藏。通过使用单一演进版本,GraphQL API 使得应用始终能够使用新的特性,并鼓励使用更加简洁、更好维护的服务端代码。
使用你现有的数据和代码
GraphQL 让你的整个应用共享一套 API,而不用被限制于特定存储引擎。GraphQL 引擎已经有多种语言实现,通过 GraphQL API 能够更好利用你的现有数据和代码。你只需要为类型系统的字段编写函数,GraphQL 就能通过优化并发的方式来调用它们。
以上内容来自于:https://graphql.cn/。
GraphQL 的中文入门文档,请参阅 https://graphql.cn/learn/;
可见,GraphQL 可以充当客户端和现有系统之间的接口,能够很方便地集成现有系统。
Go GraphQL 快速入门
关于各种编程语言的 GraphQL 实现,请参阅官方网站(https://graphql.org/code/)。接下来我们看一个 Go graphql-go/graphql (https://github.com/graphql-go/graphql)示例。
环境说明
操作系统:macOS 12.6
创建测试项目
mkdir graphql-democd graphql-demogo mod init graphql-demogo get github.com/graphql-go/graphql
项目结构
% tree ..├── go.mod├── go.sum├── gql_type│ ├── mutation_type.go│ ├── post.go│ ├── post_type.go│ ├── query_type.go│ ├── user.go│ └── user_type.go├── main.go└── schema.gql1 directory, 10 files
module graphql-demogo 1.19require github.com/graphql-go/graphql v0.8.0
package gql_typeimport ("github.com/graphql-go/graphql""strconv")var MutationType = graphql.NewObject(graphql.ObjectConfig{Name: "Mutation",Fields: graphql.Fields{"createUser": &graphql.Field{Type: UserType,Args: graphql.FieldConfigArgument{"email": &graphql.ArgumentConfig{Description: "New User Email",Type: graphql.NewNonNull(graphql.String),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {email := p.Args["email"].(string)user := &User{Email: email,}InsertUser(user)return user, nil},},"removeUser": &graphql.Field{Type: graphql.Boolean,Args: graphql.FieldConfigArgument{"id": &graphql.ArgumentConfig{Description: "User ID to remove",Type: graphql.NewNonNull(graphql.ID),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {i := p.Args["id"].(string)id, err := strconv.Atoi(i)if err != nil {return nil, err}RemoveUserByID(id)return true, nil},},"createPost": &graphql.Field{Type: PostType,Args: graphql.FieldConfigArgument{"user": &graphql.ArgumentConfig{Description: "Id of user creating the new post",Type: graphql.NewNonNull(graphql.ID),},"title": &graphql.ArgumentConfig{Description: "New post title",Type: graphql.NewNonNull(graphql.String),},"body": &graphql.ArgumentConfig{Description: "New post body",Type: graphql.NewNonNull(graphql.String),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {i := p.Args["user"].(string)userID, err := strconv.Atoi(i)if err != nil {return nil, err}title := p.Args["title"].(string)body := p.Args["body"].(string)post := &Post{UserID: userID,Title: title,Body: body,}InsertPost(post)return post, nil},},"removePost": &graphql.Field{Type: graphql.Boolean,Args: graphql.FieldConfigArgument{"id": &graphql.ArgumentConfig{Description: "Post ID to remove",Type: graphql.NewNonNull(graphql.ID),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {i := p.Args["id"].(string)id, err := strconv.Atoi(i)if err != nil {return nil, err}RemovePostByID(id)return true, err},},},})
package gql_typeimport ("errors""sync")type Post struct {ID intUserID intTitle stringBody string}var postMtx sync.RWMutexvar posts = make(map[int]*Post)var postID = 0func InsertPost(post *Post) {postMtx.Lock()defer postMtx.Unlock()postID += 1post.ID = postIDposts[post.ID] = post}func RemovePostByID(id int) {postMtx.Lock()defer postMtx.Unlock()delete(posts, id)}func GetPostByID(id int) (*Post, error) {postMtx.RLock()defer postMtx.RUnlock()post, found := posts[id]if !found {return nil, errors.New("not found")}return post, nil}func GetPostsForUser(userID int) []*Post {postMtx.RLock()defer postMtx.RUnlock()var res []*Postfor _, v := range posts {if v.UserID == userID {res = append(res, v)}}return res}
package gql_typeimport ("github.com/graphql-go/graphql")var PostType = graphql.NewObject(graphql.ObjectConfig{Name: "Post",Fields: graphql.Fields{"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if post, ok := p.Source.(*Post); ok {return post.ID, nil}return nil, nil},},"title": &graphql.Field{Type: graphql.NewNonNull(graphql.String),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if post, ok := p.Source.(*Post); ok {return post.Title, nil}return nil, nil},},"body": &graphql.Field{Type: graphql.NewNonNull(graphql.ID),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if post, ok := p.Source.(*Post); ok {return post.Body, nil}return nil, nil},},},})func init() {PostType.AddFieldConfig("user", &graphql.Field{Type: graphql.NewNonNull(UserType),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if post, ok := p.Source.(*Post); ok {return GetUserByID(post.UserID)}return nil, nil},})}
gql_type/query_type.go:
package gql_typeimport ("github.com/graphql-go/graphql""strconv")var QueryType = graphql.NewObject(graphql.ObjectConfig{Name: "Query",Fields: graphql.Fields{"user": &graphql.Field{Type: UserType,Args: graphql.FieldConfigArgument{"id": &graphql.ArgumentConfig{Description: "User ID",Type: graphql.NewNonNull(graphql.ID),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {i := p.Args["id"].(string)id, err := strconv.Atoi(i)if err != nil {return nil, err}return GetUserByID(id)},},},})
package gql_typeimport ("errors""sync")type User struct {ID intEmail string}var userMtx sync.RWMutexvar users = make(map[int]*User)var userID = 0func InsertUser(user *User) {userMtx.Lock()defer userMtx.Unlock()userID += 1user.ID = userIDusers[user.ID] = user}func RemoveUserByID(id int) {userMtx.Lock()defer userMtx.Unlock()delete(users, id)}func GetUserByID(id int) (*User, error) {userMtx.RLock()defer userMtx.RUnlock()user, found := users[id]if !found {return nil, errors.New("not found")}return user, nil}
gql_type/user_type.go:
package gql_typeimport ("github.com/graphql-go/graphql""strconv")var UserType = graphql.NewObject(graphql.ObjectConfig{Name: "User",Fields: graphql.Fields{"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if user, ok := p.Source.(*User); ok {return user.ID, nil}return nil, nil},},"email": &graphql.Field{Type: graphql.NewNonNull(graphql.String),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if user, ok := p.Source.(*User); ok {return user.Email, nil}return nil, nil},},},})func init() {UserType.AddFieldConfig("post", &graphql.Field{Type: PostType,Args: graphql.FieldConfigArgument{"id": &graphql.ArgumentConfig{Description: "Post ID",Type: graphql.NewNonNull(graphql.ID),},},Resolve: func(p graphql.ResolveParams) (interface{}, error) {i := p.Args["id"].(string)id, err := strconv.Atoi(i)if err != nil {return nil, err}return GetPostByID(id)},})UserType.AddFieldConfig("posts", &graphql.Field{Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(PostType))),Resolve: func(p graphql.ResolveParams) (interface{}, error) {if user, ok := p.Source.(*User); ok {return GetPostsForUser(user.ID), nil}return []*Post{}, nil},})}
main.go:
package mainimport ("encoding/json""github.com/graphql-go/graphql"gqlType "graphql-demo/gql_type""io/ioutil""log""net/http")func handler(schema graphql.Schema) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {query, err := ioutil.ReadAll(r.Body)if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}result := graphql.Do(graphql.Params{Schema: schema,RequestString: string(query),})w.Header().Set("Content-Type", "application/json")w.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(result)}}func main() {schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: gqlType.QueryType,Mutation: gqlType.MutationType,})if err != nil {log.Fatal(err)}http.Handle("/graphql", handler(schema))log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))}
schema.gql:
type User {id: IDemail: String!post(id: ID!): Postposts: [Post!]!}type Post {id: IDuser: User!title: String!body: String!}type Query {user(id: ID!): User}type Mutation {createUser(email: String!): UserremoveUser(id: ID!): BooleancreatePost(user: ID!, title: String!, body: String!): PostremovePost(id: ID!): Boolean}
Try it out
运行 GraphQL 服务:
% go run main.go在另一个终端运行测试:
% curl -X POST http://127.0.0.1:8080/graphql -d 'mutation {createUser(email:"[email protected]"){id, email}}'{"data":{"createUser":{"email":"[email protected]","id":"1"}}}% curl -XPOST http://127.0.0.1:8080/graphql -d 'mutation {createUser(email:"[email protected]o"){id, email}}'{"data":{"createUser":{"email":"[email protected]","id":"2"}}}% curl -XPOST http://127.0.0.1:8080/graphql -d 'mutation {createPost(user:1,title:"p1",body:"b1"){id}}'{"data":{"createPost":{"id":"1"}}}% curl -XPOST http://127.0.0.1:8080/graphql -d 'mutation {createPost(user:1,title:"p2",body:"b2"){id}}'{"data":{"createPost":{"id":"2"}}}% curl -XPOST http://127.0.0.1:8080/graphql -d '{user(id:1){id,email,posts{id,title,body}}}'{"data":{"user":{"email":"[email protected]","id":"1","posts":[{"body":"b1","id":"1","title":"p1"},{"body":"b2","id":"2","title":"p2"}]}}}% curl -XPOST http://127.0.0.1:8080/graphql -d 'mutation {removePost(id:2)}'{"data":{"removePost":true}}% curl -XPOST http://127.0.0.1:8080/graphql -d '{user(id:1){id,email,posts{id,title,body}}}'{"data":{"user":{"email":"[email protected]","id":"1","posts":[{"body":"b1","id":"1","title":"p1"}]}}}
参考文档
1. https://github.com/topliceanu/graphql-go-example
关于Portal Lab
星阑科技 Portal Lab 致力于前沿安全技术研究及能力工具化。主要研究方向为API 安全、应用安全、攻防对抗等领域。实验室成员研究成果曾发表于BlackHat、HITB、BlueHat、KCon、XCon等国内外知名安全会议,并多次发布开源安全工具。未来,Portal Lab将继续以开放创新的态度积极投入各类安全技术研究,持续为安全社区及企业级客户提供高质量技术输出。