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接口助力电商购物安全
亲爱的网购达人们,你们是否曾经因为网络上的虚假信息和诈骗而感到困扰?在享受便捷的网购乐趣时,如何确保交易安全成为了我们共同关注的话题。今天,一起来了解一下翔云身份证实名认证接口如何为电子商务保驾护航,让您的…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
leetcode73-矩阵置零
leetcode 73 思路 记录 0 元素的位置:遍历整个矩阵,找出所有值为 0 的元素,并将它们的坐标记录在数组zeroPosition中置零操作:遍历记录的所有 0 元素位置,将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...
