几个有趣的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,一经查实,立即删除!

相关文章

Qt之explicit作用及用法

在 Qt 中,explicit 是一个关键字,用于修饰类构造函数。explicit 的主要作用是防止隐式类型转换,即禁止编译器使用该构造函数进行隐式类型转换。 在 C 中,当一个构造函数只接受一个参数的时候,它可能会被用于执行隐式类…

微信小程序 ---- 通过 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、扩容 问题&…

单测结果不稳定的终极解决方案(Maven单测参数调优)

一、前言 近期,在公司平台执行单测任务时,我发现到一个显著的问题:我们的一个应用,在公司平台上执行单测时,即使是相同的代码,每次的执行结果(包括行覆盖率以及单测通过率)都存在差…

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 清空当前…

LDD学习笔记 -- Linux设备驱动概述

LDD学习笔记 -- Linux设备驱动概述 概述分类字符设备驱动块设备驱动 设备文件 概述 设备驱动:配置和管理设备的一段代码。 负责与硬件设备进行交互,并导出应用程序和其他内核模块可以用来访问设备的接口。 该代码能够通过向设备发送数据来配置设备&am…

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. 教育影响 五、…

Vue 3.4 正式版发布,带来多项更新

12 月 28 日,Vue 3.4 正式版发布,代号为“🏀 Slam Dunk”,即灌篮高手。据尤大接收,这个版本进行了许多重要的内部改进,其中最引人瞩目的是重写的模板解析器。新的解析器将速度提高了 2 倍,显著提升了整体性能。 此外,响应性系统也经过了重构,使得 effect 触发更为精…

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

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

SQL BETWEEN 操作符

BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。 SQL BETWEEN 语法 SELECT column1, column2, ... FROM table_name WHERE column BETWEEN value1 AND value2; 参数说明: column1, column2, ...:要选择的字段名…