开篇先吐槽几句~

我个人有一些习惯, 比如在服务设计时会考虑的比较长远,会考虑到到未来的扩展等等…然后程序设计的抽象成度就会比较高,各个模块之间解耦,但这样往往就会带来程序的复杂度提升。

这其实在一些公司里面是不被喜欢的, 因为这可能会延长开发周期(主要的), 增加开发成本, 以及其他同学接手项目是的学习成本。

interface多了确实对一个初接手项目的同学不太友好,找起来对应的实现真的是太麻烦了, 大家应该都有这个感觉吧?

现实的场景是大家往往都忙着交差, 代码丝毫没有设计(可能也有,但不多),能跑能实现功能就行。 特别是ToB模式的公司, 往往项目/客户需求驱动, 项目周期短, 代码质量也就不会太高。

我就在这样的公司, 这里的工作模式/交互模式甚是让我苦恼。

我对于项目整体的规范性和整洁性有比较高的要求, 所以对抽象也比较热衷。

其实他说的也对,我也认同, 但是我还是有我自己的执念吧~ (或者说强迫症)。而且把自己的理念强加给别人我觉得也不太好~

下面开始正题吧,聊聊几个有趣的框架。因为大家的功能都比较完备, 所以讨论更偏向于设计理念吧。挑了几个我认为比较好玩的框架和大家分享。

【go-kit】 一个将抽象设计暴露给用户的框架

githut: https://github.com/go-kit/kit?tab=readme-ov-file 第一眼看见go-kit的时候就觉得: 哇,这个框架也与我抽象的理念太符合了吧。 后来觉得, 太抽象了… 确实开发的复杂度变高了. (或者简单来说, 麻烦了~)

go-kit里面以Transport,Endpoint,Service,Middleware等等概念来抽象服务, 以及服务之间的调用。

Transport 负责对外暴露服务,对不同的协议的提供支持, 现在支持http,grpc,thrift等等Endpoint 很关键的一层抽象, 真实提供功能的对象屏蔽掉, 以此来实现解耦和对各种不同协议的支持Service 真正提供功能的对象, 也就是我们平时写的业务逻辑Middleware 包裹endpoint, 用于实现日志,监控,限流等等

graph LR

cliet[http、grpc、...] -->

Transport --> Endpoint

Endpoint --> Service

假如我们要实现一个服务, 提供字符串的大小写转换和计数功能。

在service中编写功能逻辑:

import "context"

// interface是抽象的关键

type StringService interface {

Uppercase(string) (string, error)

Count(string) int

}

type stringService struct{}

func (stringService) Uppercase(s string) (string, error) {

if s == "" {

return "", ErrEmpty

}

return strings.ToUpper(s), nil

}

func (stringService) Count(s string) int {

return len(s)

}

var ErrEmpty = errors.New("Empty string")

然后显示声明request和response的结构体:

我觉得显示声明是必要的, 因为这样可以让我们更清晰的知道我们的服务提供了什么功能, 以及对应的输入输出是什么。

type uppercaseRequest struct {

S string `json:"s"`

}

type uppercaseResponse struct {

V string `json:"v"`

Err string `json:"err,omitempty"`

}

type countRequest struct {

S string `json:"s"`

}

type countResponse struct {

V int `json:"v"`

}

这时功能已经有了, 但是不可以直接用service提供服务, 因为样会打破抽象, 所以需要定一个endpoint作为中间层, endpoint的定义是这样的:

可以看见request和response都是interface。

type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

定义一个我们自己的endpoint:

每个函数都需要去做转换, 是不是已经感觉到麻烦了?

import (

"context"

"github.com/go-kit/kit/endpoint"

)

func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {

return func(_ context.Context, request interface{}) (interface{}, error) {

req := request.(uppercaseRequest)

v, err := svc.Uppercase(req.S)

if err != nil {

return uppercaseResponse{v, err.Error()}, nil

}

return uppercaseResponse{v, ""}, nil

}

}

func makeCountEndpoint(svc StringService) endpoint.Endpoint {

return func(_ context.Context, request interface{}) (interface{}, error) {

req := request.(countRequest)

v := svc.Count(req.S)

return countResponse{v}, nil

}

}

目前已经抽象好了我们的服务, 接下来需要把它暴露出去, 那么就需要transport了:

其实也可用mvc的模式去理解, 只不过抽象带来的复杂度比较高~

import (

"context"

"encoding/json"

"log"

"net/http"

httptransport "github.com/go-kit/kit/transport/http"

)

func main() {

svc := stringService{}

uppercaseHandler := httptransport.NewServer(

makeUppercaseEndpoint(svc),

decodeUppercaseRequest,

encodeResponse,

)

countHandler := httptransport.NewServer(

makeCountEndpoint(svc),

decodeCountRequest,

encodeResponse,

)

http.Handle("/uppercase", uppercaseHandler)

http.Handle("/count", countHandler)

log.Fatal(http.ListenAndServe(":8080", nil))

}

// 去解码请求, 其实相当于mvc中的controller

func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {

var request uppercaseRequest

if err := json.NewDecoder(r.Body).Decode(&request); err != nil {

return nil, err

}

return request, nil

}

func decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {

var request countRequest

if err := json.NewDecoder(r.Body).Decode(&request); err != nil {

return nil, err

}

return request, nil

}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {

return json.NewEncoder(w).Encode(response)

}

那如何定义一个中间件呢? 那可真是杨宗纬的《洋葱》啊~

func loggingMiddleware(logger log.Logger) Middleware {

return func(next endpoint.Endpoint) endpoint.Endpoint {

return func(ctx context.Context, request interface{}) (interface{}, error) {

logger.Log("msg", "calling endpoint")

defer logger.Log("msg", "called endpoint")

return next(ctx, request)

}

}

}

// 然后在transport中使用包裹后的endpoint

var uppercase endpoint.Endpoint

uppercase = makeUppercaseEndpoint(svc)

uppercase = loggingMiddleware(log.With(logger, "method", "uppercase"))(uppercase)

也可以用结构体的方式:

其实所有框架的实现逻辑都差不多, 只不过go-kit更多的暴露给了用户, 其他的框架将其隐藏在了自己的实现中。

type loggingMiddleware struct {

logger log.Logger

next StringService

}

func (mw loggingMiddleware) Uppercase(s string) (output string, err error) {

defer func(begin time.Time) {

mw.logger.Log(

"method", "uppercase",

"input", s,

"output", output,

"err", err,

"took", time.Since(begin),

)

}(time.Now())

output, err = mw.next.Uppercase(s)

return

}

func (mw loggingMiddleware) Count(s string) (n int) {

defer func(begin time.Time) {

mw.logger.Log(

"method", "count",

"input", s,

"n", n,

"took", time.Since(begin),

)

}(time.Now())

n = mw.next.Count(s)

return

}

最终的目录结构可能是这个样子:

.

├── README.md

├── cmd

│ ├── addcli

│ │ └── addcli.go

│ └── addsvc

│ └── addsvc.go

├── pb

│ ├── addsvc.pb.go

│ ├── addsvc.proto

│ └── compile.sh

└── pkg

├── addendpoint

│ ├── middleware.go

│ └── set.go

├── addservice

│ ├── middleware.go

│ └── service.go

└── addtransport

├── grpc.go

├── http.go

└── jsonrpc.go

如果你赞同go-kit的这种理念, 但是又觉得麻烦, 可以看看https://github.com/nytimes/gizmo这个项目。他封装了一些复杂的、繁琐的,在go-kit中需要自己实现的逻辑。

【gotalk】 一个使用tcp实现的双向通信框架

github: https://github.com/rsms/gotalk 这个框架也挺有意思, 官方给的使用示例是这样的:

type GreetIn struct {

Name string `json:"name"`

}

type GreetOut struct {

Greeting string `json:"greeting"`

}

// 服务端代码

func server() {

gotalk.Handle("greet", func(in GreetIn) (GreetOut, error) {

return GreetOut{"Hello " + in.Name}, nil

})

if err := gotalk.Serve("tcp", "localhost:1234"); err != nil {

log.Fatalln(err)

}

}

// 客户端代码

func client() {

s, err := gotalk.Connect("tcp", "localhost:1234")

if err != nil {

log.Fatalln(err)

}

greeting := &GreetOut{}

if err := s.Request("greet", GreetIn{"Rasmus"}, greeting); err != nil {

log.Fatalln(err)

}

log.Printf("greeting: %+v\n", greeting)

s.Close()

}

乍一看, 没有什么特殊的地方嘛。 他的特别之处在于它数据传输的方式:

Gotalk采用双向和并发通信, 他并不是基于http或者grpc这些现有的应用层协议,而是使用tcp为基础, 自己实现一套通信规则。

Gotalk协议的传输格式是基于ASCII的。例如,一条代表操作请求的协议消息:r0001005hello00000005world。 正是因为这种特性, 它可以轻松实现一个websocket服务:

而且gotalk还有对应的js客户端…

package main

import (

"net/http"

"github.com/rsms/gotalk"

)

func main() {

gotalk.Handle("echo", func(in string) (string, error) {

return in, nil

})

// 注册websocket服务

http.Handle("/gotalk/", gotalk.WebSocketHandler())

http.Handle("/", http.FileServer(http.Dir(".")))

err := http.ListenAndServe("localhost:1234", nil)

if err != nil {

panic(err)

}

}

消息最终会被转换为字节进行传输,消息包含自己的载荷类型、操作类型、载荷长度和载荷本身等。

比如单次请求得内容可能是这样的:

+------------------ SingleRequest

| +---------------- requestID "0001"

| | +--------- operation "echo" (text3Size 4, text3Value "echo")

| | | +- payloadSize 25

| | | |

r0001004echo00000019{"message":"Hello World"}

响应是这样的:

+------------------ SingleResult

| +---------------- requestID "0001"

| | +-------- payloadSize 25

| | |

R000100000019{"message":"Hello World"}

也可以发起流式的请求:

+------------------ StreamRequest

| +---------------- requestID "0001"

| | +--------- operation "echo" (text3Size 4, text3Value "echo")

| | | +- payloadSize 11

| | | |

s0001004echo0000000b{"message":

+------------------ streamReqPart

| +---------------- requestID "0001"

| | +-------- payloadSize 14

| | |

p00010000000e"Hello World"}

+------------------ streamReqPart

| +---------------- requestID "0001"

| | +-------- payloadSize 0 (end of stream)

| | |

p000100000000

流式的的响应:

+------------------ StreamResult (1st part)

| +---------------- requestID "0001"

| | +-------- payloadSize 11

| | |

S00010000000b{"message":

+------------------ StreamResult (2nd part)

| +---------------- requestID "0001"

| | +-------- payloadSize 14

| | |

S00010000000e"Hello World"}

+------------------ StreamResult

| +---------------- requestID "0001"

| | +-------- payloadSize 0 (end of stream)

| | |

S000100000000

【goa】 一个有代码生成器并且使用简单编码的方式来描述功能的框架

github: [goa](https://github.com/goadesign/goa)

这个项目理念上其实与go-kit比较类似,但是他没有transport的概念, 而且因为有生成工具的存在, 所以使用起来比较方便,省掉了跟多重复的编码工作。

它可以使用一段go代码来描述服务的所要实现的功能, 生成工具(goa)以此来生成服务代码。

mkdir -p calcsvc/design

cd calcsvc

go mod init calcsvc

比如要生成一个实现计算器功能的服务, 需要创建一个design/design.go:

package design

import . "goa.design/goa/v3/dsl"

// 描述这个api的属性信息

var _ = API("calc", func() {

Title("Calculator Service")

Description("HTTP service for multiplying numbers, a goa teaser")

Server("calc", func() {

Host("localhost", func() { URI("http://localhost:8088") })

})

})

// 描述服务需要实现的功能细节

var _ = Service("calc", func() {

Description("The calc service performs operations on numbers")

// 一个名字为"multiply"的方法

Method("multiply", func() {

// 方法接收的载荷

Payload(func() {

// 一个int类型的a字段

Attribute("a", Int, "Left operand")

// 一个int类型的b字段

Attribute("b", Int, "Right operand")

// Required表示这个字段是必须的

Required("a", "b")

})

// 返回结果是一个int

Result(Int)

// 这提供http服务

HTTP(func() {

// GET请求的路径,并从路径中获取a和b的值

GET("/multiply/{a}/{b}")

// 返回状态码

Response(StatusOK)

})

})

})

使用命令生成服务代码:

goa gen calcsvc/design

会生成如下的目录结构:

gen

├── calc

│ ├── client.go

│ ├── endpoints.go

│ └── service.go

└── http

├── calc

│ ├── client

│ │ ├── cli.go

│ │ ├── client.go

│ │ ├── encode_decode.go

│ │ ├── paths.go

│ │ └── types.go

│ └── server

│ ├── encode_decode.go

│ ├── paths.go

│ ├── server.go

│ └── types.go

├── cli

│ └── calc

│ └── cli.go

├── openapi.json

└── openapi.yaml

7 directories, 15 files

目前生成的代码还不能直接使用,可以理解为是一些抽象出来的框架, 需要我们自己实现服务的功能。 运行生成示例代码的命令, 会根据定义生成程序入口和功能代码的文件:

goa example calcsvc/design

运行后在目录中会多出以下几个文件:

calc.go

cmd/calc-cli/http.go

cmd/calc-cli/main.go

cmd/calc/http.go

cmd/calc/main.go

calc.go是我们的功能实现文件:

package calcapi

import (

calc "calcsvc/gen/calc"

"context"

"log"

)

// calc service example implementation.

// The example methods log the requests and return zero values.

type calcsrvc struct {

logger *log.Logger

}

// NewCalc returns the calc service implementation.

func NewCalc(logger *log.Logger) calc.Service {

return &calcsrvc{logger}

}

// Multiply implements multiply.

func (s *calcsrvc) Multiply(ctx context.Context, p *calc.MultiplyPayload) (res int, err error) {

// 填写自己的功能实现

s.logger.Print("calc.multiply")

return

}

然后就可运行服务/客户端了:

# 服务端

cd cmd/calc

go build

./calc

[calcapi] 16:10:47 HTTP "Multiply" mounted on GET /multiply/{a}/{b}

[calcapi] 16:10:47 HTTP server listening on "localhost:8088"

# 客户端

cd calcsvc/cmd/calc-cli

go build

./calc-cli calc multiply -a 2 -b 3

6

其他的一些框架

一些功能完备,开发成本较低的框架:

go-micro 自带认证、配置热加载、服务发现、多种通信协议等等kratos 通过protobuf的定义实现http/grpc服务,支持多种config源,支持trancing等等,并且使用Wire 进行依赖注入。go-zero 功能十分全面, 但是使用起来比较复杂,好在文档丰富,并且有自己的生成工具,

框架其实非常多了, 上面列举的三个是star数量比较多的, 你也可以在github上使用 language:go microservices的方式搜索, 会有很多的结果。

参考文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: