软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

Golang服务常用组件—Gin, JSON, GraphQL, gRPC

给前端使用的服务接口,一般采用JSONGraphQL格式。微服务间的服务接口,一般采用JSON或gRPC格式。

Gin是一个非常流行的HTTP服务框架,在GitHub上的Stars已有32k。在提供JSON或GraphQL格式的服务接口时,Gin是一个非常明智的选择。在提供gRPC格式的服务接口时,使用官方的工具protoc-gen-go自动生成代码也比较便捷。

格式 优点 缺点 选择时考虑点
JSON 最通用;数据自描述 数组时冗余数据大;序列化效率不高 不需要schema,能自动识别属性与数据;JSON序列化不是性能问题点
GraphQL 一次请求获取所需的、最少的所有数据;自带输入数据校验 本身也是JSON格式,且更高层次的封装,性能比单纯的JSON略差 前端使用,想提高网络访问性能;服务聚合
gRPC 高效的序列化,字节流最小 需要schema才能解析数据的含义;不通用 使用JSON格式时,网络传输与序列化出现性能问题时,就该换到gRPC

1. Gin

Gin是一个快速路由的Web框架,其使用类似NodeJS或前端的Middleware模式,可扩展性强;其使用也很容易,跟NodeJS中Rest框架很类似。官网,见https://gin-gonic.com/

当使用jsoniter做JSON序列化处理时,go build -tags=jsoniter .

如下代码中,我们把路由处理均包含到mainRouter()函数中,可以方便后面做http服务的单元测试。

// main.go
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

prefixV1 = "/api/v1"

func main() {
    listeningAddress := ":8080"
    mainRouter().Run(listeningAddress)
}

func mainRouter() *gin.Engine {
    engine := gin.Default()

    engine.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    // group
    v1 := engine.Group(prefixV1)

    v1.GET("/foo", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "bar",
        })
    })

    v1.GET("/bar", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "foo",
        })
    })

    return engine
}
// main_test.go
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/stretchr/testify/assert"
)

func Test_mainRouter(t *testing.T) {
    // new server
    router := mainRouter()
    ts := httptest.NewServer(router)
    defer ts.Close()

    // request
    url := fmt.Sprintf("%s%s", ts.URL, "/ping")
    req, err := http.NewRequest("GET", url, nil)
    resp, err := client.Do(req)

    if err != nil {
        fmt.Println(err)
    }

    // response
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    got := string(body)

    want := `{"message":"Pong"}`
    assert.Equal(t, want, got)
}

2. JSON

GoLang内部本身就支持JSON处理。但GoLang 1.30.x之前处理JSON时性能比较差,有人提供了一个高效的JSON处理库json-iterator,见https://github.com/json-iterator/go。其100%兼容GoLang自带的encoding/json

用法:


import (
    jsoniter "github.com/json-iterator/go"
)

type UserInfo struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var json = jsoniter.ConfigCompatibleWithStandardLibrary

...

func foo() {
    userInfo := UserInfo{ 1000, "User 1" }
    json.Marshal(&userInfo)
}

func bar() {
    input := []byte(`{"id": 1000, "name": "User 1", "email": ""}`)
    var userInfo UserInfo
    json.Unmarshal(input, &userInfo)
}

3. GraphQL

GraphQL是我们设计BFF (Backends For Frontends)系统时使用的一种接口实现。在要求比较高的用户体验、又要提升开发效率时,GraphQL是目前的唯一选择。

在GoLang中,我对比了几个GraphQL库,发现https://github.com/99designs/gqlgen可以快速上手、且开发效率比较高的库。特性对比,见 https://gqlgen.com/feature-comparison/

参考https://gqlgen.com/getting-started/, 定义好schema.graphql之后,执行go run github.com/99designs/gqlgen init可以帮你生成很多GraphQL处理代码,自己只需要完成resolver.go各个具体类型数据的获取。完整自动生成的文件:generated.go、models_gen.go

// resolver.go
...

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    todo := &Todo{
        Text: input.Text,
        // ID:     fmt.Sprintf("T%d", rand.Int()),
        UserID: input.UserID,
    }

    saveNewTodo(ctx, todo)
    // r.todos = append(r.todos, todo)
    return todo, nil
}

func (r *mutationResolver) UpdateTodo(ctx context.Context, input UpdateTodoInfo) (*Todo, error) {
    todo := &Todo{
        ID:   input.ID,
        Text: input.Text,
        Done: input.Done,
    }

    return updateTodo(ctx, todo)
}

Gin中集成gqlgen:

import (
    "github.com/99designs/gqlgen/handler"
    "github.com/gin-gonic/gin"
)

// Defining the Graphql handler
func graphqlHandler() gin.HandlerFunc {
    // NewExecutableSchema and Config are in the generated.go file
    // Resolver is in the resolver.go file
    h := handler.GraphQL(NewExecutableSchema(Config{Resolvers: &Resolver{}}))

    return func(c *gin.Context) {
        h.ServeHTTP(c.Writer, c.Request)
    }
}

// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
    h := handler.Playground("GraphQL", prefixV1+"/query")

    return func(c *gin.Context) {
        h.ServeHTTP(c.Writer, c.Request)
    }
}

func main() {
    engine := gin.New()

    engine.POST("/query", graphqlHandler())
    engine.GET("/", playgroundHandler())
}

4. gRPC

gRPC传输格式是Protocol Buffers,相比其他格式,其序列化效率比较高,传输的字节流也比较小。在GoLang中使用gRPC,目前使用官方提供的工具是最方便的。

安装部署,见 https://grpc.io/docs/quickstart/go/ ,先安装好protoc,再执行go get -u -v github.com/golang/protobuf/protoc-gen-go。Go Module中,需要注意GFW,见 https://github.com/grpc/grpc-go#faq ,把gRPC的引用从Google切换到GitHub,``。

定义好.proto文件之后,执行protoc -I pb/ --go_out=plugins=grpc:pb ./pb/helloworld.proto,自动生成helloworld.pb.go,其中包含类型定义、proto处理等,我们只需完成每个请求的数据回应。

// go.mod
module mytest

go 1.12

require (
    ...
    github.com/golang/protobuf v1.3.2
    google.golang.org/grpc v1.22.0
)

replace google.golang.org/grpc v1.22.0 => github.com/grpc/grpc-go v1.22.0
// main.go in server side
package main

import (
    "google.golang.org/grpc"
)

const (
    port = ":8080"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}


func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello again " + in.Name}, nil
}

func (s *server) SayHelloStream(req *pb.HelloRequest, srv pb.Greeter_SayHelloStreamServer) error {
    srv.Send(&pb.HelloReply{Message: "Hello stream[1] " + req.GetName()})
    srv.Send(&pb.HelloReply{Message: "Hello stream[2] " + req.GetName()})
    return nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer(grpc.UnaryInterceptor(apmgrpc.NewUnaryServerInterceptor()))
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
// main.go in client side

func fetchProduct(ctxParent context.Context) string {
    // Set up a connection to the server.
    conn, err := grpc.Dial(productServerAddress, grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(apmgrpc.NewUnaryClientInterceptor()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(ctxParent, time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)

    result := r.Message

    r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
    return result
}