一套代码,同时启动 7 种 Golang Web 框架

1,136 阅读5分钟

今天我们来做一个有趣的 Go 实践。使用同一套代码,在一个进程中,同时启动 7种不同的 Go Web 框架。

为什么做这么无聊的事儿?

主要目的就是介绍 rookie-ninja/rk-boot 库。

启动哪些 Go Web 框架?

我们同时启动如下几个 Go Web 框架。

Web 框架rk-boot 依赖版本
gin-gonic/gingo get github.com/rookie-ninja/rk-boot/ginv1.2.14 (Stable)
gRPCgo get github.com/rookie-ninja/rk-boot/grpcv1.2.18 (Stable)
labstack/echogo get github.com/rookie-ninja/rk-boot/echov0.0.8 (Stable)
gogf/gfgo get github.com/rookie-ninja/rk-boot/gfv0.0.6 (Stable)
gofiber/fibergo get github.com/rookie-ninja/rk-boot/fiberv0.0.4 (Testing)
zeromicro/go-zerogo get github.com/rookie-ninja/rk-boot/zerov0.0.2 (Testing)
gorilla/muxgo get github.com/rookie-ninja/rk-boot/muxv0.0.2 (Testing)

快速开始

1.安装

我们通过 go get 安装如下依赖。

go get github.com/rookie-ninja/rk-boot/grpc
go get github.com/rookie-ninja/rk-boot/gin
go get github.com/rookie-ninja/rk-boot/echo
go get github.com/rookie-ninja/rk-boot/gf
go get github.com/rookie-ninja/rk-boot/fiber
go get github.com/rookie-ninja/rk-boot/zero
go get github.com/rookie-ninja/rk-boot/mux

2.创建 boot.yaml

Web 框架端口
gRPC8081
gin-gonic/gin8082
labstack/echo8083
gogf/gf8084
gofiber/fiber8085
zeromicro/go-zero8086
gorilla/mux8087

除了指定端口,我们还开启了如下两个选项:

  • commonService: 提供 /rk/v1/healthy 这类通用 API。
  • loggingZap: RPC 日志
---
grpc:
  - name: grpc
    port: 8081
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
gin:
  - name: gin
    port: 8082
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
echo:
  - name: echo
    port: 8083
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
gf:
  - name: gf
    port: 8084
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
fiber:
  - name: fiber
    port: 8085
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
zero:
  - name: zero
    port: 8086
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false
mux:
  - name: mux
    port: 8087
    enabled: true
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      loggingZap:
        enabled: true                 # Optional, default: false

3.创建 main.go

所有 Web 框架都是用 handleRequest() 函数来处理请求。

gRPC 例外,我们没有使用 protocol buffer,因为 PB 的协议不同。我们使用 grpc-gateway 来模拟。

grpcEntry 默认会开启 grpc-gateway,通过 grpcEntry.HttpMux 往 grpc-gateway 里注册 API。

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gofiber/adaptor/v2"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/labstack/echo/v4"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/echo"
	"github.com/rookie-ninja/rk-boot/fiber"
	"github.com/rookie-ninja/rk-boot/gf"
	"github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-boot/grpc"
	"github.com/rookie-ninja/rk-boot/mux"
	"github.com/rookie-ninja/rk-boot/zero"
	"github.com/tal-tech/go-zero/rest"
	"net/http"
)

const handlePath = "/v1/hello"

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// 6: go-zero @8086, must adds route before bootstrap
	rkbootzero.GetZeroEntry("zero").Server.AddRoute(rest.Route{
		Method:  http.MethodGet,
		Path:    handlePath,
		Handler: handleRequest,
	})

	// Bootstrap
	boot.Bootstrap(context.Background())

	// 1: grpc-gateway @8081
	rkbootgrpc.GetGrpcEntry("grpc").HttpMux.HandleFunc(handlePath, handleRequest)
	// 2: gin @8082
	rkbootgin.GetGinEntry("gin").Router.Handle(http.MethodGet, handlePath, gin.WrapF(handleRequest))
	// 3: echo @8083
	rkbootecho.GetEchoEntry("echo").Echo.GET(handlePath, echo.WrapHandler(http.HandlerFunc(handleRequest)))
	// 4: GoFrame @8084
	rkbootgf.GetGfEntry("gf").Server.BindHandler(handlePath, ghttp.WrapF(handleRequest))
	// 5: Fiber @8085, must call RefreshFiberRoutes()
	rkbootfiber.GetFiberEntry("fiber").App.Get(handlePath, adaptor.HTTPHandler(http.HandlerFunc(handleRequest)))
	rkbootfiber.GetFiberEntry("fiber").RefreshFiberRoutes()
	// 7: mux @8087
	rkbootmux.GetMuxEntry("mux").Router.HandleFunc(handlePath, handleRequest)

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

// Handle request for all web frameworks
func handleRequest(writer http.ResponseWriter, req *http.Request) {
	// 1: get query
	name := req.URL.Query().Get("name")

	// 2: marshal response
	bytes, _ := json.Marshal(&Response{
		Message: fmt.Sprintf("Hello %s", name),
	})

	// 3: write response
	writer.WriteHeader(http.StatusOK)
	writer.Write(bytes)
}

type Response struct {
	Message string `json:"message"`
}

4.文件夹结构 & go.mod

  • 文件夹结构
.
├── boot.yaml
├── go.mod
├── go.sum
└── main.go

0 directories, 4 files
  • go.mod 文件
module github.com/rookie-ninja/rk-demo

go 1.16

require (
	github.com/gin-gonic/gin v1.7.7
	github.com/gofiber/adaptor/v2 v2.1.15
	github.com/gogf/gf/v2 v2.0.0-beta
	github.com/labstack/echo/v4 v4.6.1
	github.com/rookie-ninja/rk-boot v1.4.0
	github.com/rookie-ninja/rk-boot/echo v0.0.8
	github.com/rookie-ninja/rk-boot/fiber v0.0.4
	github.com/rookie-ninja/rk-boot/gf v0.0.6
	github.com/rookie-ninja/rk-boot/gin v1.2.14
	github.com/rookie-ninja/rk-boot/grpc v1.2.18
	github.com/rookie-ninja/rk-boot/mux v0.0.2
	github.com/rookie-ninja/rk-boot/zero v0.0.2
	github.com/tal-tech/go-zero v1.2.4
)

5.启动 main.go

$ go run main.go

2022-01-03T19:15:14.016+0800    INFO    boot/gf_entry.go:1050   Bootstrap gfEntry       {"eventId": "264033ff-be9c-4147-871c-e4873ea1510d", "entryName": "gf"}
------------------------------------------------------------------------
endTime=2022-01-03T19:15:14.016473+08:00
startTime=2022-01-03T19:15:14.016057+08:00
elapsedNano=416178
timezone=CST
ids={"eventId":"264033ff-be9c-4147-871c-e4873ea1510d"}
app={"appName":"rk","appVersion":"","entryName":"gf","entryType":"GfEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"commonServiceEnabled":true,"commonServicePathPrefix":"/rk/v1/","gfPort":8084}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost
operation=Bootstrap
resCode=OK
eventStatus=Ended
EOE
...

调用 /rk/v1/healthy 来确定服务是否启动。

$ curl localhost:8081/rk/v1/healthy
{"healthy":true}

$ curl localhost:8082/rk/v1/healthy
{"healthy":true}

$ curl localhost:8083/rk/v1/healthy
{"healthy":true}

$ curl localhost:8084/rk/v1/healthy
{"healthy":true}

$ curl localhost:8085/rk/v1/healthy
{"healthy":true}

$ curl localhost:8086/rk/v1/healthy
{"healthy":true}

$ curl localhost:8087/rk/v1/healthy
{"healthy":true}

6.验证

我们将会发送 /v1/hello 请求到各个端口,并查看 RPC 日志。

  • grpc-gateway@8081 (grpc-gateway 目前暂无 RPC 日志输出)
$ curl "localhost:8081/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}
  • gin@8082
$ curl "localhost:8082/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:18:26.23983+08:00
startTime=2022-01-03T19:18:26.239805+08:00
elapsedNano=25460
timezone=CST
ids={"eventId":"0d284016-f714-4c85-8af8-9715dc9ed35f"}
app={"appName":"rk","appVersion":"","entryName":"gin","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:54102
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE
  • echo@8083
$ curl "localhost:8082/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:19:11.109838+08:00
startTime=2022-01-03T19:19:11.109817+08:00
elapsedNano=21242
timezone=CST
ids={"eventId":"34419c7c-1a78-484f-ba7a-bfca69178b82"}
app={"appName":"rk","appVersion":"","entryName":"echo","entryType":"EchoEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:56995
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE
$ curl "localhost:8084/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:19:39.905883+08:00
startTime=2022-01-03T19:19:39.905858+08:00
elapsedNano=24703
timezone=CST
ids={"eventId":"58bf8706-09ff-434e-b405-d6cdb8dbe8c2"}
app={"appName":"rk","appVersion":"","entryName":"gf","entryType":"GfEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:58802
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE
  • fiber@8085
$ curl "localhost:8085/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:20:48.834567+08:00
startTime=2022-01-03T19:20:48.834425+08:00
elapsedNano=142332
timezone=CST
ids={"eventId":"a98a6e9f-6519-4ded-971e-0b6e59f66096"}
app={"appName":"rk","appVersion":"","entryName":"fiber","entryType":"FiberEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"http","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=127.0.0.1:63237
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE
  • zero@8086
$ curl "localhost:8086/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:21:20.835415+08:00
startTime=2022-01-03T19:21:20.835391+08:00
elapsedNano=24495
timezone=CST
ids={"eventId":"a6a53d21-4cf4-4b45-97ca-2b190e438e9c","traceId":"bf7a2359d0813de4388dd11c4f161321"}
app={"appName":"rk","appVersion":"","entryName":"zero","entryType":"ZeroEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:65299
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE
  • mux@8087
$ curl "localhost:8086/v1/hello?name=rk-dev"
{"message":"Hello rk-dev"}

# RPC log from server side
------------------------------------------------------------------------
endTime=2022-01-03T19:22:13.13191+08:00
startTime=2022-01-03T19:22:13.131889+08:00
elapsedNano=21449
timezone=CST
ids={"eventId":"8a0f2db6-8e13-4773-bedd-962060adbe41"}
app={"appName":"rk","appVersion":"","entryName":"mux","entryType":"MuxEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/hello","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=127.0.0.1:52277
operation=/v1/hello
resCode=200
eventStatus=Ended
EOE

rk-boot 介绍

rk-boot 是一个可通过 YAML 启动多种 Web 服务的框架。 有点类似于 Spring boot。通过集成 rk-xxx 系列库,可以启动多种 Web 框架。当然,用户也可以自定义 rk-xxx 库集成到 rk-boot 中。

rk-boot 亮点

  • 通过同样格式的 YAML 文件,启动不同 Web 框架。
  • 即使是不同框架,统一日志,Metrics, Tracing 等格式
  • 用户可以通过实现 rkentry.Entry 来自定义 YAML 文件。

rk-boot 支持的 Web 框架

欢迎贡献新的 Web 框架到 rk-boot 系列中。

参考 docs & rk-gin 作为例子。

框架开发状态安装依赖
GinStablego get github.com/rookie-ninja/rk-boot/ginrk-gin
gRPCStablego get github.com/rookie-ninja/rk-boot/grpcrk-grpc
EchoStablego get github.com/rookie-ninja/rk-boot/echork-echo
GoFrameStablego get github.com/rookie-ninja/rk-boot/gfrk-gf
FiberTestinggo get github.com/rookie-ninja/rk-boot/fiberrk-fiber
go-zeroTestinggo get github.com/rookie-ninja/rk-boot/zerork-zero
GorillaMuxTestinggo get github.com/rookie-ninja/rk-boot/muxrk-mux