go项目中比较好的实践方案
工作两年来,我并未遇到太大的挑战,也没有特别值得夸耀的项目。尽管如此,在日常的杂项工作中,我积累了不少心得,许多实践方法也在思考中逐渐得到优化。因此,我在这里记录下这些心得。
转发与封装
这个需求相当常见,包括封装上游的请求、接收下游的响应,并将它们封装后发送给上游:
这里展示的是一个简单的代理模型。乍一看,这可能是一个简单的需求,只需两个函数封装即可。但当需要扩展其他额外需求时,这里的设计就显得尤为重要。例如:server
需要支持流式协议、proxy
需要进行鉴权和计费、proxy
需要支持多个接口转发、proxy
需要支持限流等。
借助第三方代理封装
有些方案可以直接借鉴。如果仅需要支持HTTP协议,我们可以直接使用httputil.ReverseProxy
。在Director
中定义wrap request
行为,在ModifyResponse
中定义wrap response
行为。这是我们在内部的openai代理项目中采用的思路。以下是简单的代码示例:
director := func(req *http.Request) {// 读取并重新填充请求体body, _ := io.ReadAll(req.Body)// 转换请求体req.Body = io.NopCloser(bytes.NewBuffer(body))// 转换头部req.Header.Set("KEY", "Value")req.Header.Del("HEADER")// 转换URLoriginURL := req.URL.String()req.Host = remote.Hostreq.URL.Scheme = remote.Schemereq.URL.Host = remote.Hostreq.URL.Path = path.Join("redirected", req.URL.Path)req.URL.RawPath = req.URL.EscapedPath()// 转换查询参数query := req.URL.Query()query.Add("ExtraHeader", "Value")req.URL.RawQuery = query.Encode()
}modifyResponse := func(resp *http.Response) error {// 记录失败的请求查询和响应if resp.StatusCode < 200 || resp.StatusCode >= 300 {}// 使用一些操作包装io.reader,例如将数据转储到数据库,记录令牌和计费,ReadWrapper应实现io.Reader接口resp.Body = &ReadWrapper{reader: resp.Body,}return nil
}
p := &httputil.ReverseProxy{Director: director,ModifyResponse: modifyResponse,
}
这里只需专注于实现业务代码,不需关心如何发送和接收数据包,这也符合Go语言基于接口编程的思想。
自己如何实现
但如果是其他协议,例如websocket
、rpc
等,可能也存在类似好用的util
,例如websocketproxy,实现思路和上面的代码片段一致。但对于其他协议,可能没有好用的第三方库,我们就需要自己实现。
为了兼容流式和非流式,我们最初的实现是使用协程:
errCh := make(chan error)
respCh := make(chan []byte)
go RequestServer(ctx, reqBody, respCh, errCh)
loop:
for {select {case resp, ok := <-respCh:if !ok {break loop}// 封装响应并发送case err, ok := <-errCh:// 封装错误并发送}
}
协程用于请求server
,接收并封装response
,然后通过管道发送到主逻辑,主逻辑负责与client
通信。这里乍一看没什么问题,但引入了一个协程和两个管道,导致程序的复杂度大大提高。后来我们进行了改进,将管道换成可异步读写的缓冲区:
var buf Buffer
go RequestServer(ctx, reqBody, &buf)
for {n, err := buf.Read(chunk)if err != nil {if err != io.EOF {// 封装错误并发送}return}if n > 0 {// 封装响应并发送}
}
这里的逻辑稍微清晰一些,只引入了一个协程,主逻辑几乎不用怎么更改。
还可以更优雅。社区建议我们放心大胆使用goroutine,但并不希望我们滥用。Practical Go:
if your goroutine cannot make progress until it gets the result from another, oftentimes it is simpler to just do the work yourself rather than to delegate it.
This often eliminates a lot of state tracking and channel manipulation required to plumb a result back from a goroutine to its initiator.
“如果主逻辑要从另一个 goroutine 获得结果才能取得进展,那么主逻辑自己完成工作通常比委托他人更简单。”。其实我们是更希望消除这个goroutine的,像之前的 httputil.ReverseProxy
一样,我们可以把逻辑封装成 io.ReadCloser
接口,然后返回到主逻辑:
type WrappedReader struct {rawReader io.ReadCloser // response.Body
}func (r *WrappedReader) Read(p []byte) (int, error) {raw := make([]byte, cap(p))n, err := r.rawReader.Read(raw)// 封装响应
}
func (r *WrappedReader) Close() error {return r.rawReader.Close()
}wrappedReader, err := ConnectServer(ctx, reqBody)
if err != nil {return err
}
defer wrappedReader.Close()
for {n, err := wrappedReader.Read(chunk)if err != nil {if err != io.EOF {return err}return nil}if n > 0 {// 发送响应}
}
这样,这个版本就全部改成了同步逻辑,不存在异步通信!并且在扩展类似计费、限流、鉴权等功能时,不会污染转发的主逻辑。但这里需要注意io.Reader
接口的定义,实现时需要满足接口定义的具体行为,之前的项目也踩过一次坑。
之前的一个项目接入层使用的是第二种方案,当时我还觉得自己的设计很优雅,将很多个转发协议整合到一个接口定义上,大大缩减了开发和维护人力成本。后来,我从这个项目转到另一个项目,现在再去看之前的设计,发现这个接口已经从原来的4个方法膨胀到7个方法了。之前基于接口开发的优雅设计如今一定会被后面的开发者所憎恨,因为实现一个简单的转发接口一定要求你实现7个方法。现在分析下来,还是之前的接口定义不合理,之前的接口定义wrap response
的方法为:
type Forwarder interface {// ...WrapInferResp(p []byte) []byte// ...
}
所有的下游连接使用的都是基于标准HTTP的方法进行连接,所以后面需要兼容其他下游协议时就需要堆方法到接口定义中,因为这里传入的都是接口对象。如果将上面的方法改为:
type Forwarder interface {// ...Read(p []byte) (int, error)// ...
}
其实也就是io.Reader
定义,我们这里就可以把连接下游的具体行为放到结构体定义去了,具体使用什么协议都可以实现。
这里给我们的提示其实就是,在定义接口时尽量多考虑更抽象更底层的行为,也就是go中已有的接口定义,通过这些接口组合得到最终的接口,这样可能往往是较好的设计。
配置文件
在go中,我们写入配置到内存,一般是有环境变量、监听远程下发、本地配置文件、主动读取数据库这几类。一般配置文件用于存放数据量不大,但变动较频繁的配置。
配置文件的读取
一般配置文件的路径会写成相对路径,方便本地调试与线上部署,读取的代码一般放在 config
模块的init()
函数中。配置文件放到 workspace
的根目录下:
package configimport ("fmt""os""path"
)const configPath = "./config.json"func init() {content, err := os.ReadFile(configPath)if err != nil {panic(err)}// 解析并设置内存全局配置变量
}
程序运行不会有什么问题,但是在做单元测试的时候就很难受了,因为我们在做单元测试的时候需要在目标模块的目录下,例如我们在下面的项目中对模块 moduleA
执行单元测试:
./
├── go.mod
├── go.sum
├── main.go
├── config.json
├── moduleA
│ ├── submodule1.go
│ ├── Test_submodule1.go
├── config
│ ├── config.gocd moduleA
go test -v
这时候配置文件的读取就会失败,因为我们这里使用的相对路径,可能会有人提议,那不能使用绝对路径吗?如果使用绝对路径,那么这个路径就需要配置化,那么就要配置文件或者环境变量,部署的复杂度就大些了。否则就直接hardcode ,需要在发到线上生产环境前修改变量,这种情况下如果不CR就很容易出错。
因此这里应该容许读取配置文件时,可以在多个文件夹下寻找配置文件:
package configimport ("fmt""path""github.com/spf13/viper"
)var (G *viper.ViperWorkspace string
)func init() {G = viper.New()G.SetConfigName("config") // 配置文件的名称(不带扩展名)G.SetConfigType("yaml")G.AddConfigPath("../") // 查找配置文件的路径G.AddConfigPath(".") //err := G.ReadInConfig() // 查找并加载配置文件if err != nil { //panic(fmt.Errorf("fatal error config file: %w", err))}Workspace = path.Dir(G.ConfigFileUsed())fmt.Println("=== Workspace:", Workspace)
}
这里的 viper
就可以支持在多个路径下查找文件,非常方便,让我们在模块的 init
函数中可以大胆使用相对路径的方式读取文件。
配置文件的格式
配置文件的格式一般会有很多种,像json、yaml、toml,一般项目中用的比较多是json和yaml,然后在代码中定义对应的结构体定义,例如:
type limitationConfig struct {Key string `yaml:"key,omitempty"`NParallel int32 `yaml:"nparallel,omitempty"`
}
这里会有一个问题,扩展起来很麻烦,你需要修改结构体的定义,如果涉及到结构体的嵌套,配置参数较多,就会存在一个庞大的结构体定义。其实很多时候,这些配置项本身之间没有很大关联,只是为了减少配置文件的数量,都放到同一个配置文件中,yaml格式本身就是将各个配置项解耦的。而viper
是可以允许无结构体定义直接读取配置项的,例如:
var G *viper.Viper
region := config.G.GetString("host.region")
namespace := config.G.GetString("namespace")
值得赞许的是,这里读取配置项时,不用处理error!这个真的是goher的救星好吗。因此使用viper
+yaml格式应该是对开发者来说比较舒服的方式。
JSON序列化
go官方自带的encoding/json
包对于更细微的序列化格式调整支持的不是很好,例如会将HTML字符序列化成unicode格式,默认不支持缩进,只能根据jsontag决定是否渲染缺省值(这一点在我的另一篇博客中有详细说明)等。这里安利一个第三方的sdk json-iterator/go
,例如:
import jsoniter "github.com/json-iterator/go"var json = jsoniter.Config{IndentionStep: 2,EscapeHTML: true,SortMapKeys: true,ValidateJsonRawMessage: true,
}.Froze()type NotOmitemptyValEncoder struct {encoder jsoniter.ValEncoder
}func (codec *NotOmitemptyValEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {codec.encoder.Encode(ptr, stream)
}func (codec *NotOmitemptyValEncoder) IsEmpty(ptr unsafe.Pointer) bool {return false
}type NotOmitemptyEncoderExtension struct {jsoniter.DummyExtension
}func (extension *NotOmitemptyEncoderExtension) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder {return &NotOmitemptyValEncoder{encoder: encoder}
}func init() {jsoniter.RegisterExtension(new(NotOmitemptyEncoderExtension))
}
通过 Config
设置缩进以及是否转义,通过注入 Extension
来避免零值在序列化时被忽略。使用的语法和标准的 encoding/json
包是一致的,可以无缝替代历史代码。
未完待续
相关文章:

go项目中比较好的实践方案
工作两年来,我并未遇到太大的挑战,也没有特别值得夸耀的项目。尽管如此,在日常的杂项工作中,我积累了不少心得,许多实践方法也在思考中逐渐得到优化。因此,我在这里记录下这些心得。 转发与封装 这个需求…...

回溯法基础入门解析
回溯法 前 言 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。回溯法,一般可以解决如下几种问题: 组合问题:N个数里面按一定规则找出k个数的集合切割问题:一…...

计算机网络-VPN虚拟专用网络概述
前面我们学习了在企业内部的二层交换机网络、三层路由网络包括静态路由、OSPF、IS-IS、NAT等,现在开始学习下VPN(Virtual Private Network,虚拟专用网络),其实VPN可能很多人听到第一反应就是梯子,但是其实这…...

信创时代的数据库之路:2024 Top10 国产数据库迁移与同步指南
数据库一直是企业数字化和创新的重要基础设施之一。从传统的关系型数据库到非关系型数据库、分析型数据库,再到云数据库和多模数据库,这一领域仍在持续变革中,各种新型数据库产品涌现,数据管理的能力和应用场景也由此得到了扩展。…...

自制游戏:监狱逃亡
第一个游戏,不喜勿喷: #include<bits/stdc.h> #include<windows.h> using namespace std; int xz; int ruond_1(int n){if(xz1){printf("撬开了,但站在你面前的是俄罗斯内务部特种部队的奥摩大帝,你被九把加特…...

小雪时节,阴盛阳衰,注意禁忌
宋张嵲《小雪作》 霜风一夜落寒林,莽苍云烟结岁阴。 把镜渐无勋业念,爱山唯驻隐沦心。 冰花散落衡门静,黄叶飘零一迳深。 世乱身穷无可奈,强将悲慨事微吟。 网络图片:小雪时节 笔者禁不住喟然而叹:“冰…...

CPU性能优化--微操作
x86 架构处理器吧复杂的CISC指令转为简单的RISC微操作。这样做最大的优势是微操作可以乱序执行,一条简单的相加指令--比如ADD,EAX, EBX,只产生一个微操作,而很多复杂指令--比如ADD, EAX 可能会产生两个微操作,一个将数…...

工厂模式
主要解决对象的创建问题 首先是简单工厂 只有一个工厂类,每次有新的产品就需要修改里面接口的内容,违反了封闭原则 //1、定义抽象产品类 class AbstractCar { public:AbstractCar() default;virtual ~AbstractCar() default;virtual void showName(…...

嵌入式系统与OpenCV
目录 一、OpenCV 简介 二、嵌入式 OpenCV 的安装方法 1. Ubuntu 系统下的安装 2. 嵌入式 ARM 系统中的安装 3. Windows10 和树莓派系统下的安装 三、嵌入式 OpenCV 的性能优化 1. 介绍嵌入式平台上对 OpenCV 进行优化的必要性。 2. 利用嵌入式开发工具,如优…...

编程之路,从0开始:动态内存笔试题分析
Hello大家好,很高兴我们又见面啦! 给生活添点passion,开始今天的编程之路。 今天我们来看几个经典的动态内存笔试题。 1、题目1 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> void GetMemory(char* …...

物联网研究实训室建设方案
一、引言 随着物联网技术的快速发展,其在各个行业的应用越来越广泛,对物联网专业人才的需求也日益增加。为满足这一需求,建设一个符合现代化教学需求的物联网研究实训室,对于提高学生的实践能力和创新能力具有重要意义。本方案旨…...

Mac vscode 激活列编辑模式
列编辑模式在批量处理多行文本时,非常有效,但 vscode 默认情况下,又没有激活,因此记录一下启动方法: 激活列编辑模式 然后就可以使用 Alt(Mac 上是 Option 或 Command 键) 鼠标左键 滑动选择了…...

深度学习:GPT-1的MindSpore实践
GPT-1简介 GPT-1(Generative Pre-trained Transformer)是2018年由Open AI提出的一个结合预训练和微调的用于解决文本理解和文本生成任务的模型。它的基础是Transformer架构,具有如下创新点: NLP领域的迁移学习:通过最…...

前端图像处理(一)
目录 一、上传 1.1、图片转base64 二、图片样式 2.1、图片边框【border-image】 三、Canvas 3.1、把canvas图片上传到服务器 3.2、在canvas中绘制和拖动矩形 3.3、图片(同色区域)点击变色 一、上传 1.1、图片转base64 传统上传: 客户端选择图片…...

unity中:超低入门级显卡、集显(功耗30W以下)运行unity URP管线输出的webgl程序有那些地方可以大幅优化帧率
删除Global Volume: 删除Global Volume是一项简单且高效的优化措施。实测表明,这一改动可以显著提升帧率,甚至能够将原本无法流畅运行的场景变得可用。 更改前的效果: 更改后的效果: 优化阴影和材质: …...

ftdi_sio应用学习笔记 4 - I2C
目录 1. 查找设备 2. 打开设备 3. 写数据 4. 读数据 5. 设置频率 6 验证 6.1 遍历设备 6.2 开关设备 6.3 读写测试 I2C设备最多有6个(FT232H),其他为2个。和之前的设备一样,定义个I2C结构体记录找到的设备。 #define FT…...

如何更好的把控软件测试质量
如何更好的把控软件测试质量 在软件开发过程中,测试是确保软件质量、稳定性和用户体验的重要环节。随着需求的不断变化以及技术的不断进步,如何更好的把控软件测试质量已成为一个不可忽视的话题。本文将从几个维度探讨确保软件质量的方法和方案…...

“漫步北京”小程序及“气象景观数字化服务平台”上线啦
随着科技的飞速发展,智慧旅游已成为现代旅游业的重要趋势。近日,北京万云科技有限公司联合北京市气象服务中心,打造的“气象景观数字化服务平台“和“漫步北京“小程序已经上线,作为智慧旅游的典型代表,以其丰富的功能…...

SOL链上的 Meme 生态发展:从文化到创新的融合#dapp开发#
一、引言 随着区块链技术的不断发展,Meme 文化在去中心化领域逐渐崭露头角。从 Dogecoin 到 Shiba Inu,再到更多细分的 Meme 项目,这类基于网络文化的加密货币因其幽默和社区驱动力吸引了广泛关注。作为近年来备受瞩目的区块链平台之一&…...

身份证实名认证API接口助力电商购物安全
亲爱的网购达人们,你们是否曾经因为网络上的虚假信息和诈骗而感到困扰?在享受便捷的网购乐趣时,如何确保交易安全成为了我们共同关注的话题。今天,一起来了解一下翔云身份证实名认证接口如何为电子商务保驾护航,让您的…...

【过程控制系统】第6章 串级控制系统
目录 6. l 串级控制系统的概念 6.1.2 串级控制系统的组成 6.l.3 串级控制系统的工作过程 6.2 串级控制系统的分析 6.2.1 增强系统的抗干扰能力 6.2.2 改善对象的动态特性 6.2.3 对负荷变化有一定的自适应能力 6.3 串级控制系统的设计 6.3.1 副回路的选择 2.串级系…...

YOLOv11融合针对小目标FFCA-YOPLO中的FEM模块及相关改进思路
YOLOv11v10v8使用教程: YOLOv11入门到入土使用教程 YOLOv11改进汇总贴:YOLOv11及自研模型更新汇总 《FFCA-YOLO for Small Object Detection in Remote Sensing Images》 一、 模块介绍 论文链接:https://ieeexplore.ieee.org/document/10…...

qt+opengl 三维物体加入摄像机
1 在前几期的文章中,我们已经实现了三维正方体的显示了,那我们来实现让物体的由远及近,和由近及远。这里我们需要了解一个概念摄像机。 1.1 摄像机定义:在世界空间中位置、观察方向、指向右侧向量、指向上方的向量。如下图所示: …...

day05(单片机高级)PCB基础
目录 PCB基础 什么是PCB?PCB的作用? PCB的制作过程 PCB板的层数 PCB设计软件 安装立创EDA PCB基础 什么是PCB?PCB的作用? PCB(Printed Circuit Board),中文名称为印制电路板,又称印刷…...

全球天气预报5天-经纬度版免费API接口教程
接口简介: 获取全球任意地区未来5天天气预报,必须传经纬度参数。可先调用【位置坐标】分类下相关接口获取地区经纬度坐标。 请求地址: https://cn.apihz.cn/api/tianqi/tqybjw5.php 请求方式: POST或GET。 请求参数:…...

Shell编程8
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...

python语言基础-5 进阶语法-5.5 上下文管理协议(with语句)
声明:本内容非盈利性质,也不支持任何组织或个人将其用作盈利用途。本内容来源于参考书或网站,会尽量附上原文链接,并鼓励大家看原文。侵删。 5.5 上下文管理协议(with语句)(参考链接࿱…...

自动驾驶3D目标检测综述(三)
前两篇综述阅读理解放在这啦,有需要自行前往观看: 第一篇:自动驾驶3D目标检测综述(一)_3d 目标检测-CSDN博客 第二篇:自动驾驶3D目标检测综述(二)_子流行稀疏卷积 gpu实现-CSDN博客…...

【GESP】C++三级练习 luogu-B3661, [语言月赛202209] 排排
三级知识点一维数组练习,除了应用了数组以外,其余逻辑比较简单,适合初学者。 题目题解详见:https://www.coderli.com/gesp-3-luogu-b3661/ 【GESP】C三级练习 luogu-B3661, [语言月赛202209] 排排队 | OneCoder三级知识点一维数…...

【PPTist】添加PPT模版
前言:这篇文章来探索一下如何应用其他的PPT模版,给一个下拉菜单,列出几个项目中内置的模版 PPT模版数据 (一)增加菜单项 首先在下面这个菜单中增加一个“切换模版”的菜单项,点击之后在弹出框中显示所有的…...