背景
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-demo
cd graphql-demo
go mod init graphql-demo
go 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.gql
1 directory, 10 files
module graphql-demo
go 1.19
require github.com/graphql-go/graphql v0.8.0
package gql_type
import (
"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_type
import (
"errors"
"sync"
)
type Post struct {
ID int
UserID int
Title string
Body string
}
var postMtx sync.RWMutex
var posts = make(map[int]*Post)
var postID = 0
func InsertPost(post *Post) {
postMtx.Lock()
defer postMtx.Unlock()
postID += 1
post.ID = postID
posts[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 []*Post
for _, v := range posts {
if v.UserID == userID {
res = append(res, v)
}
}
return res
}
package gql_type
import (
"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_type
import (
"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_type
import (
"errors"
"sync"
)
type User struct {
ID int
Email string
}
var userMtx sync.RWMutex
var users = make(map[int]*User)
var userID = 0
func InsertUser(user *User) {
userMtx.Lock()
defer userMtx.Unlock()
userID += 1
user.ID = userID
users[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_type
import (
"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 main
import (
"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: ID
email: String!
post(id: ID!): Post
posts: [Post!]!
}
type Post {
id: ID
user: User!
title: String!
body: String!
}
type Query {
user(id: ID!): User
}
type Mutation {
createUser(email: String!): User
removeUser(id: ID!): Boolean
createPost(user: ID!, title: String!, body: String!): Post
removePost(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将继续以开放创新的态度积极投入各类安全技术研究,持续为安全社区及企业级客户提供高质量技术输出。