几个有趣的go服务框架

开篇先吐槽几句~

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

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

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 LRcliet[http、grpc、...] -->Transport --> EndpointEndpoint --> 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")

然后显示声明requestresponse的结构体:

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

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 uppercaseRequestif 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 countRequestif 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.Loggernext   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 designimport . "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")})// 返回结果是一个intResult(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.yaml7 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 calcapiimport (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的方式搜索, 会有很多的结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/596470.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序 ---- 通过 URLScheme 或 URLLink 从短信、邮件、微信外网页等场景打开小程序

1. 用于短信、邮件、网页、微信内等拉起小程序的方法 《URL Scheme 拉起小程序》《URL Link 拉起小程序》 2. 功能描述 URL Scheme: 该接口用于获取小程序 scheme 码,适用于短信、邮件、外部网页、微信内等拉起小程序的业务场景。目前仅针对国内非个人主体的小程…

奇技淫巧:如何给项目中的RabbitMQ添加总开关

本文主要分享了如何给项目中的RabbitMQ添加总开关,通过简单配置开/关RabbitMQ。 一、需求背景 SpringBoot项目里使用了RabbitMQ,但某些场景下,不希望项目启动时自动检查RabbitMQ连接 例如: 在开发不需要RabbitMQ的功能过程中&…

WEB:探索开源PDF.js技术应用

1、简述 PDF.js 是一个由 Mozilla 开发的开源 JavaScript 库,用于在浏览器中渲染 PDF 文档。它的目标是提供一个纯粹的前端解决方案,摆脱了依赖插件或外部程序的束缚,使得在任何支持 JavaScript 的浏览器中都可以轻松地显示 PDF 文档。 2、…

C语言中指针变量如何使用

一、指针变量的定义与声明 1.1 定义 指针变量是用来存储另一个变量的内存地址的变量。在C语言中,指针变量的类型是指向某个类型的指针。例如,int *p; 表示一个整型指针变量p。 1.2 声明 指针变量的声明分为两种形式,一种是直接声明&#…

linux-磁盘扩容 -- 小黑日常超细教程

hi~ 这次小黑带来的是linux磁盘扩容超细教学,按照步骤来,超容易~ 目录 模拟实验对象: 1、查看磁盘分区和挂载点 2、查看新增磁盘 3、将新磁盘格式化,建立新分区 4、查看vg卷组信息 5、分区添加卷组 6、扩容 问题&…

Android kotlin build.gradle.kts配置

1. 添加 maven 仓库 1. 1. settings配置 1. 1.1. settings.gradle repositories {maven {url https://maven.aliyun.com/repository/public/}mavenCentral() }1. 1.2. settings.gradle.kts repositories {maven {setUrl("https://maven.aliyun.com/repository/public/…

《Redis实战》学习笔记

特点 :1、是一个高性能的key/value内存型数据库 2、支持丰富的数据类型(string,List,Set,ZSet,Hash) 3、支持持久化 内存数据, 可以持久化到硬盘中 4、单进程,单线程 效率高 redis实现分布式锁 一、redis的相关指令 1、flushDB 清空当前…

Flink实时电商数仓之旁路缓存

撤回流的处理 撤回流是指流式处理过程中,两表join过程中的数据是一条一条跑过来的,即原本可以join到一起的数据在刚开始可能并没有join上。 撤回流的格式: 解决方案 定时器:使用定时器定时10s(数据最大的时间差值&am…

7.14解数独(LC37-H)

算法: 二维递归(递归时需要两层for循环) 一个for循环放行 另一个for循环放列 画树: 因为这个树形结构太大了,我抽取一部分,如图所示: 回溯三部曲: 1.确定函数参数和返回值 返…

在Gradle工程中使用checkstyle来规范你的项目

🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 系列专栏目录 [Java项…

CSS transition详解

文章目录 属性transition-propertytransition-durationtransition-timing-functiontransition-delaytransition 简写属性 方法Element:transitionrun 事件Element:transitionstart 事件Element:transitionend 事件Element:transit…

音频DAC,ADC,CODEC高性能立体声

想要让模拟信号和数字信号顺利“交往”,就需要一座像“鹊桥”一样的中介,将两种不同的语言转变成统一的语言,消除无语言障碍。这座鹊桥就是转换器芯片,也就是ADC芯片。ADC芯片的全称是Analog-to-Digital Converter, 即模拟数字转换…

【白盒测试】逻辑覆盖和路径测试的设计方法

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…

互联网演进历程:从“全球等待”到“全球智慧”的技术革新与商业变革

文章目录 一、导言二、World Wide Wait (全球等待)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 三、World Wide Web (万维网)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 四、World Wide Wisdom (全球智慧)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 五、…

Java编程中的IO模型详解:BIO,NIO,AIO的区别与实际应用场景分析

IO模型 IO模型就是说用什么样的通道进行数据的发送和接收,Java 共支持3种网络编程IO 模式:BIO,NIO,AIO BIO(Blocking lO) 同步阻塞模型, 一个客户端连接对应一个处理线程 代码示例: package com.tuling.bio; import java.io.…

DeepL翻译器,一直想使用怎么办?

作为一个独立开发者,将应用程序翻译到不同语言是个让我很头大的事情。请专业人员翻译太贵无法承受,谷歌翻译质量太差时常词不达意。 如何使用 DeepL 使用起来很直观,打开此网页粘贴要翻译的内容即可。它也支持 macOS 和 PC 端。 这里开我们开…

LinuxShell

一、 新建用户 在Linux上新建一个用户并赋予超级用户权限,建立家目录并设置默认shell为bash,并设置Linux在输入sudo密码时显示星号。请提交全部命令及输出截图(表明完成需求即可)。 1.sudo useradd -m ymhs(用户名) 增加用户 2.su…

SpingBoot的项目实战--模拟电商【5.沙箱支付】

🥳🥳Welcome Huihuis Code World ! !🥳🥳 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 🥳🥳Welcome Huihuis Code World ! !🥳🥳 一. 沙箱支付是什么 二.Sp…

2分钟了解什么是socket?

文章目录 概念比喻类型Socket 与 TCP、UDP的关系 概念 Socket 是提供网络通信功能的编程接口(API),提供了网络通信的基本操作,允许程序或进程之间进行数据交换。是传输层协议的具体软件实现,它封装了协议底层的复杂实…

【干货】Windows中定时删除system32目录下的.dmp文件教程

旭帆科技的技术人员除了给用户答疑解惑以外,还会主动测试软件性能,进行平台优化,除此之外,技术人员还会总结一些技术干货,这不,近期又提供了一份如何在Windows中定时删除system32目录下的.dmp文件的教程。感…