几个有趣的go服务框架

news2025/7/18 15:31:42

开篇先吐槽几句~

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

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

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")

然后显示声明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 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的方式搜索, 会有很多的结果。

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

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

相关文章

LeetCode 2:两数相加

一、题目描述 给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个…

Linux进程以及计划服务(二)

一.控制进程 前台运行:通过终端启动,且启动后一直占据终端(影响当前终端的操作) 后台运行:可通过终端启动,但启动后即转入后台运行(不影响当前终端的操作) 1.手动启动 前台启动&…

最优化理论期末复习笔记 Part 2

数学基础线性代数 从行的角度从列的角度行列式的几何解释向量范数和矩阵范数 向量范数矩阵范数的更强的性质的意义 几种向量范数诱导的矩阵范数 1 范数诱导的矩阵范数无穷范数诱导的矩阵范数2 范数诱导的矩阵范数 各种范数之间的等价性向量与矩阵序列的收敛性 函数的可微性与展…

Linux基础——进程初识(二)

1. 对当前目录创建文件的理解 我们知道在创建一个文件时&#xff0c;它会被默认创建到当前目录下&#xff0c;那么它是如何知道当前目录的呢&#xff1f; 对于下面这样一段代码 #include <stdio.h> #include <unistd.h>int main() {fopen("tmp.txt", …

2023 年最先进认证方式上线,Authing 推出 Passkey 无密码认证

密码并非是当前数字世界才有的安全手段。古今中外诸如故事中的《阿里巴巴与四十大盗》的“芝麻开门”口诀&#xff0c;或是江湖中“天王盖地虎&#xff0c;宝塔镇河妖”等传统的口令形式&#xff0c;都是以密码作为基本形态进行身份认证。然而&#xff0c;随着密码在越来越多敏…

ubuntu22.04配置双网卡绑定提升带宽

这里写自定义目录标题 Bonding简介配置验证参考链接 Bonding简介 bonding(绑定)是一种linux系统下的网卡绑定技术&#xff0c;可以把服务器上n个物理网卡在系统内部抽象(绑定)成一个逻辑上的网卡&#xff0c;能够提升网络吞吐量、实现网络冗余、负载均衡等功能&#xff0c;有很…

RTSP/Onvif安防平台EasyNVR接入EasyNVS显示服务不存在的原因及解决办法

EasyNVS云管理平台具备汇聚与管理EasyGBS、EasyNVR等平台的能力&#xff0c;可以将接入的视频资源实现统一的视频能力输出&#xff0c;支持远程可视化运维等管理功能&#xff0c;还能解决设备现场没有固定公网IP却需要在公网直播的需求。 有用户在现场部署EasyNVR&#xff0c;…

如何实现APP安全加固?加固技术、方法和方案

​ 本文我们着重分享App安全加固的相关内容。 ​ &#xff08;安全检测内容&#xff09; 通过前面的文章我们知道了app安全检测要去检测哪些内容&#xff0c;发现问题后我们如何去修复&#xff1f;如何避免安全问题&#xff1f;首先我们先来讲一下APP安全加固技术。 Ipa Guar…

【pdf密码】pdf文件如何限制编辑?

想要给PDF文件设置一个密码防止他人对文件进行编辑&#xff0c;那么我们可以对PDF文件设置限制编辑&#xff0c;设置方法很简单&#xff0c;我们在PDF编辑器中点击文件 – 属性 – 安全&#xff0c;在权限下拉框中选中【密码保护】 然后在密码保护界面中&#xff0c;我们勾选【…

SpringCloud Alibaba之Nacos配置中心配置详解

目录 Nacos配置中心数据模型Nacos配置文件加载Nacos配置 Nacos配置中心数据模型 Nacos 数据模型 Key 由三元组唯一确定&#xff0c;三元组分别是Namespace、Group、DataId&#xff0c;Namespace默认是公共命名空间&#xff08;public&#xff09;&#xff0c;分组默认是 DEFAUL…

22款奔驰GLE450升级香氛负离子 车载香薰

相信大家都知道&#xff0c;奔驰自从研发出香氛负离子系统后&#xff0c;一直都受广大奔驰车主的追捧&#xff0c;香氛负离子不仅可以散发出清香淡雅的香气外&#xff0c;还可以对车内的空气进行过滤&#xff0c;使车内的有害气味通过负离子进行过滤&#xff0c;达到车内保持清…

CentOS7部署Kafka

CentOS7部署Kafka 一、部署1、前置条件2、下载与解压3、修改配置4、启动kafka二、使用详解1、创建一个主题2、展示所有主题3、启动消费端接收消息4、生产端发送消息三、代码集成pom.xmlapplication.propertiesKafkaConfiguration.javaKafkaConsumer.javaKafkaProducer.javaVehi…

【算法挨揍日记】day34——647. 回文子串、5. 最长回文子串

647. 回文子串 647. 回文子串 题目描述&#xff1a; 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串&am…

MATLAB基本绘图操作(二维和三维绘图)

MATLAB基本绘图操作 文章目录 MATLAB基本绘图操作1、二维平面绘图1.1、线条&#xff08;折线图&#xff09;1.2、条形图1.3、极坐标图1.4、散点图 2、三维立体绘图2.1、三维曲面图2.2、三维曲线图&#xff08;点图&#xff09; 3、图片分区&#xff08;子图&#xff09; 1、二维…

【springboot项目】之秒杀项目常见问题(Seckill)

秒杀问题分为两部分&#xff1a;用户查看商品详情页、用户下单 项目简介&#xff1a; 模拟了高并发场景的商城系统&#xff0c;它具备秒杀功能&#xff0c;为了解决秒杀场景下的高并发问题。引入了 redis 作为缓存中间件&#xff0c;1.主要作用是缓存预热、预减库存等等。2.针…

简易五子棋的实现(C++)

名人说&#xff1a;莫听穿林打叶声&#xff0c;何妨吟啸且徐行。—— 苏轼《定风波莫听穿林打叶声》 Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#xff09; 目录 一、效果图二、代码&#xff08;带注释&#xff09;三、说明 一、效果图 二、代码&#xff08;带…

获取CNN/DM适用于评估Bart的格式的数据集(类似于test.source、test.source.tokenized)

项目场景&#xff1a; 复现文本摘要任务评估CNN/DM数据集 问题描述 abisee老哥的代码获取的是bin格式的数据集 时间久远&#xff0c;一些依赖的配置版本难以复现 笔者需要能评估Bart 格式的数据集 形式类似于test.source、test.source.tokenized 解决方案&#xff1a; 经过坚…

确定转角起始扭矩值的方法有哪些

在预紧螺栓时&#xff0c;扭矩加角度法是一种常用的方法。这种方法需要确定转角起始扭矩值&#xff0c;以确保螺栓能够被正确地预紧。确定转角起始扭矩值的方法如下&#xff0c;SunTorque智能扭矩系统带大家一起了解。1. 确定螺栓规格和性能参数 在预紧螺栓之前&#xff0c;需要…

odoo17 | 模型之间的关系

前言 上一章介绍了自定义的创建 包含基本字段的模型的视图。但是&#xff0c;在任何实际业务场景中&#xff0c;我们需要的不仅仅是 一个模型。此外&#xff0c;模型之间的链接是必要的。人们可以很容易地想象一个模型包含 客户和另一个包含用户列表的客户。您可能需要推荐客户…

分布式图文详解!

分布式理论 1. 说说CAP原则&#xff1f; CAP原则又称CAP定理&#xff0c;指的是在一个分布式系统中&#xff0c;Consistency&#xff08;一致性&#xff09;、 Availability&#xff08;可用性&#xff09;、Partition tolerance&#xff08;分区容错性&#xff09;这3个基本…