go项目中比较好的实践方案

news2024/11/26 0:14:00

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

转发与封装

这个需求相当常见,包括封装上游的请求、接收下游的响应,并将它们封装后发送给上游:

在这里插入图片描述

这里展示的是一个简单的代理模型。乍一看,这可能是一个简单的需求,只需两个函数封装即可。但当需要扩展其他额外需求时,这里的设计就显得尤为重要。例如: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")
    // 转换URL
    originURL := req.URL.String()
    req.Host = remote.Host
    req.URL.Scheme = remote.Scheme
    req.URL.Host = remote.Host
    req.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语言基于接口编程的思想。

自己如何实现

但如果是其他协议,例如websocketrpc等,可能也存在类似好用的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 config

import (
	"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.go

cd moduleA
go test -v

这时候配置文件的读取就会失败,因为我们这里使用的相对路径,可能会有人提议,那不能使用绝对路径吗?如果使用绝对路径,那么这个路径就需要配置化,那么就要配置文件或者环境变量,部署的复杂度就大些了。否则就直接hardcode ,需要在发到线上生产环境前修改变量,这种情况下如果不CR就很容易出错。

因此这里应该容许读取配置文件时,可以在多个文件夹下寻找配置文件:

package config

import (
	"fmt"
	"path"

	"github.com/spf13/viper"
)

var (
	G         *viper.Viper
	Workspace 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 包是一致的,可以无缝替代历史代码。

未完待续

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

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

相关文章

Maven的安装——给Idea配置Maven

一、什么是Maven? Maven是一个开源的项目管理工具&#xff0c;它主要用于Java项目的构建、依赖管理和项目生命周期管理。 二、准备环境 maven安装之前&#xff0c;我们要先安装jdk&#xff0c;确保你已经安装了jdk环境。可以通过【win】【r】打开任务管理器&#xff0c;输入…

vscode 远程连接ssh 密钥方式

目录 1. powershell 生成key&#xff1a; 2. 在服务器上安装公钥 linux测试成功&#xff1a; 3).为了确保连接成功&#xff0c;输入如下指令以保证以下文件权限正确&#xff1a; 3 开启 ssh 密钥登录 vscode 远程连接配置 python连接测试ok 查看日志&#xff1a; 命令…

Charles抓包工具-笔记

摘要 概念&#xff1a; Charles是一款基于 HTTP 协议的代理服务器&#xff0c;通过成为电脑或者浏览器的代理&#xff0c;然后截取请求和请求结果来达到分析抓包的目的。 功能&#xff1a; Charles 是一个功能全面的抓包工具&#xff0c;适用于各种网络调试和优化场景。 它…

Echarts+VUE饼图的使用(基础使用、多个饼图功能、单组饼图对应颜色使用)

安装&#xff1a;npm install echarts --save 配置:main.js // 引入echarts import * as echarts from echarts Vue.prototype.$echarts echarts一、基础饼图&#xff08;直接拷贝就能出效果&#xff09; <div class"big-box" ref"demoEhart"><…

神经网络(系统性学习三):多层感知机(MLP)

相关文章&#xff1a; 神经网络中常用的激活函数 神经网络&#xff08;系统性学习一&#xff09;&#xff1a;入门篇 神经网络&#xff08;系统性学习二&#xff09;&#xff1a;单层神经网络&#xff08;感知机&#xff09; 多层感知机&#xff08;MLP&#xff09; 多层感…

C语言练习.if.else语句.strstr

今天在做题之前&#xff0c;先介绍一下&#xff0c;新学到的库函数strstr 想要使用它&#xff0c;要先给它一个头文件<string.h> char *strstr(const char*str1,const char*str2); 首先&#xff1a;1.strstr的返回值是char&#xff0c;字符类型的。 2.两个实参&#xff…

golang实现TCP服务器与客户端的断线自动重连功能

1.服务端 2.客户端 生成服务端口程序: 生成客户端程序: 测试断线重连: 初始连接成功

c语言数据结构与算法--简单实现线性表(顺序表+链表)的插入与删除

老规矩&#xff0c;点赞评论收藏关注&#xff01;&#xff01;&#xff01; 目录 线性表 其特点是&#xff1a; 算法实现&#xff1a; 运行结果展示 链表 插入元素&#xff1a; 删除元素&#xff1a; 算法实现 运行结果 线性表是由n个数据元素组成的有限序列&#xff…

textureLod lod的确定

1. 自动计算导数方法 float calculateLOD(sampler2D tex, vec2 uv) { // 计算纹理坐标的导数 vec2 dUVdx dFdx(uv); vec2 dUVdy dFdy(uv); // 计算纹理大小 vec2 textureSize textureSize(tex, 0); // 0表示基础mipmap级别 // 计算LOD float maxDeriv max(length(…

C++数据结构与算法

C数据结构与算法 1.顺序表代码模版 C顺序表模版 #include <iostream> using namespace std; // 可以根据需要灵活变更类型 #define EleType intstruct SeqList {EleType* elements;int size;int capacity; };// Init a SeqList void InitList(SeqList* list, int capa…

SSH 反向端口转发-R

近期和同学聊到了他遇到的一个问题&#xff1a; 本地机器A&#xff0c;远程开发机C&#xff0c;机器A需要通过ssh连接开发机C进行开发&#xff0c;ssh直连不通&#xff0c;SCP传输文件的话就比较费劲&#xff0c;需要通过跳板机B&#xff0c;经过跳板机这一步骤可用&#xff0…

实时质检系统—静音检测功能设置流程

设置流程 1. 设置静音检测时间 在实时质检系统中&#xff0c;有一静音检测功能&#xff1a;可以对主叫或被叫在接通后的规定时间内开启静音检测。例&#xff1a;被叫静音检测配置设置了10&#xff0c;那么质检电话在接通后的10秒内&#xff0c;开启静音检测&#xff0c;如果被…

生成式UI 动态化SDK的研发(二)--实现垂直布局、水平布局以及文字组件

文章目录 1. 概述2. 效果展示2.1 垂直布局容器(Column)2.2 水平布局容器(Row )2.3 本文示例动态化模板DSL 3. 生成式UI 动态化SDK的渲染流程4. Column和Row组件实现4.1 Column组件4.1.1 视图的渲染4.1.2 事件解析绑定 4.2 Row组件4.3 文字组件(Text) 5. 源码地址 1. 概述 在前…

JavaScript中的this指向绑定规则(超全)

JavaScript中的this指向绑定规则&#xff08;超全&#xff09; 1.1 为什么需要this? 为什么需要this? 在常见的编程语言中&#xff0c;几乎都有this这个关键字&#xff08;Objective-C中使用的是self),但是在JavaScript中的this和常见的面向对象语言中的this不太一样 常见面…

Vision Transformer(VIT模型)

【11.1 Vision Transformer(vit)网络详解-哔哩哔哩】 https://b23.tv/BgsYImJ 工作流程&#xff1a; ①将输入的图像进行patch的划分 ②Linear Projection of Flatted patches&#xff0c;将patch拉平并进行线性映射生成token ③生成CLS token&#xff08;用向量有效地表示整…

2024年11月最新 Alfred 5 Powerpack (MACOS)下载

在现代数字化办公中&#xff0c;我们常常被繁杂的任务所包围&#xff0c;而时间的高效利用成为一项核心需求。Alfred 5 Powerpack 是一款专为 macOS 用户打造的高效工作流工具&#xff0c;以其强大的定制化功能和流畅的用户体验&#xff0c;成为众多效率爱好者的首选。 点击链…

C#里怎么样检测文件的属性?

C#里怎么样检测文件的属性? 对于文件来说,在C#里有一种快速的方法来检查文件的属性。 比如文件是否已经压缩, 文件是否加密, 文件是否是目录等等。 属性有下面这么多: 例子演示如下: /** C# Program to View the Information of the File*/ using System; using Syste…

网络安全,文明上网(4)掌握网络安全技术

前言 在数字化时代&#xff0c;个人信息和企业数据的安全变得尤为重要。为了有效保护这些宝贵资产&#xff0c;掌握一系列网络安全技术是关键。 核心技术及实施方式 1. 网络监控与过滤系统&#xff1a; 这些系统构成了网络防御体系的基石&#xff0c;它们负责监控网络通信&…

Vue 项目中如何使用FullCalendar 时间段选择插件(类似会议室预定、课程表)

本文中是基于VUEelementui项目中实现的前后端分离的前端功能部分&#xff1a; 插件的官方文档&#xff1a;FullCalendar 1.安装对应依赖&#xff08;统一安装版本为6.15&#xff09; npm install --save fullcalendar/core6.15 npm install --save fullcalendar/daygrid6.…

Oracle SQL优化③——表的连接方式

前言 表&#xff08;结果集&#xff09;与表&#xff08;结果集&#xff09;之间的连接方式非常重要&#xff0c;如果CBO选择了错误的连接方式&#xff0c;本来几秒就能出结果的SQL可能执行一天都执行不完。如果想要快速定位超大型SQL性能问题&#xff0c;就必须深入理解表连接…