Golang服务常用组件—Gin, JSON, GraphQL, gRPC
给前端使用的服务接口,一般采用JSON或GraphQL格式。微服务间的服务接口,一般采用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
}