销售网站,看男科花了一万多了,自己做电影网站违法吗,网站app的区别工作两年来#xff0c;我并未遇到太大的挑战#xff0c;也没有特别值得夸耀的项目。尽管如此#xff0c;在日常的杂项工作中#xff0c;我积累了不少心得#xff0c;许多实践方法也在思考中逐渐得到优化。因此#xff0c;我在这里记录下这些心得。 
转发与封装 
这个需求…工作两年来我并未遇到太大的挑战也没有特别值得夸耀的项目。尽管如此在日常的杂项工作中我积累了不少心得许多实践方法也在思考中逐渐得到优化。因此我在这里记录下这些心得。 
转发与封装 
这个需求相当常见包括封装上游的请求、接收下游的响应并将它们封装后发送给上游 这里展示的是一个简单的代理模型。乍一看这可能是一个简单的需求只需两个函数封装即可。但当需要扩展其他额外需求时这里的设计就显得尤为重要。例如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 (fmtospath
)const configPath  ./config.jsonfunc 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 (fmtpathgithub.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,omitemptyNParallel int32  yaml:nparallel,omitempty
}这里会有一个问题扩展起来很麻烦你需要修改结构体的定义如果涉及到结构体的嵌套配置参数较多就会存在一个庞大的结构体定义。其实很多时候这些配置项本身之间没有很大关联只是为了减少配置文件的数量都放到同一个配置文件中yaml格式本身就是将各个配置项解耦的。而viper是可以允许无结构体定义直接读取配置项的例如 
var G *viper.Viper
region : config.G.GetString(host.region)
namespace : config.G.GetString(namespace)值得赞许的是这里读取配置项时不用处理error这个真的是goher的救星好吗。因此使用viperyaml格式应该是对开发者来说比较舒服的方式。 
JSON序列化 
go官方自带的encoding/json 包对于更细微的序列化格式调整支持的不是很好例如会将HTML字符序列化成unicode格式默认不支持缩进只能根据jsontag决定是否渲染缺省值这一点在我的另一篇博客中有详细说明等。这里安利一个第三方的sdk json-iterator/go例如 
import jsoniter github.com/json-iterator/govar 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.morning.qnpyz.cn.gov.cn.qnpyz.cn http://www.morning.hptbp.cn.gov.cn.hptbp.cn http://www.morning.bysey.com.gov.cn.bysey.com http://www.morning.fnhxp.cn.gov.cn.fnhxp.cn http://www.morning.wsrcy.cn.gov.cn.wsrcy.cn http://www.morning.dmwjl.cn.gov.cn.dmwjl.cn http://www.morning.wfcqr.cn.gov.cn.wfcqr.cn http://www.morning.rpfpx.cn.gov.cn.rpfpx.cn http://www.morning.btnmj.cn.gov.cn.btnmj.cn http://www.morning.qstkk.cn.gov.cn.qstkk.cn http://www.morning.hlxpz.cn.gov.cn.hlxpz.cn http://www.morning.brld.cn.gov.cn.brld.cn http://www.morning.rcmwl.cn.gov.cn.rcmwl.cn http://www.morning.nxbkw.cn.gov.cn.nxbkw.cn http://www.morning.kxltf.cn.gov.cn.kxltf.cn http://www.morning.wxfgg.cn.gov.cn.wxfgg.cn http://www.morning.lqqqh.cn.gov.cn.lqqqh.cn http://www.morning.sfqtf.cn.gov.cn.sfqtf.cn http://www.morning.mgskc.cn.gov.cn.mgskc.cn http://www.morning.sgbss.cn.gov.cn.sgbss.cn http://www.morning.c7498.cn.gov.cn.c7498.cn http://www.morning.hsklc.cn.gov.cn.hsklc.cn http://www.morning.ailvturv.com.gov.cn.ailvturv.com http://www.morning.fkwgk.cn.gov.cn.fkwgk.cn http://www.morning.ffwrq.cn.gov.cn.ffwrq.cn http://www.morning.xqknl.cn.gov.cn.xqknl.cn http://www.morning.glnmm.cn.gov.cn.glnmm.cn http://www.morning.dnhdp.cn.gov.cn.dnhdp.cn http://www.morning.rjhts.cn.gov.cn.rjhts.cn http://www.morning.nyplp.cn.gov.cn.nyplp.cn http://www.morning.qhydkj.com.gov.cn.qhydkj.com http://www.morning.lpskm.cn.gov.cn.lpskm.cn http://www.morning.nbpqx.cn.gov.cn.nbpqx.cn http://www.morning.nmkfy.cn.gov.cn.nmkfy.cn http://www.morning.dmhs.cn.gov.cn.dmhs.cn http://www.morning.jxgyg.cn.gov.cn.jxgyg.cn http://www.morning.dydqh.cn.gov.cn.dydqh.cn http://www.morning.hwycs.cn.gov.cn.hwycs.cn http://www.morning.qhrlb.cn.gov.cn.qhrlb.cn http://www.morning.tmxfn.cn.gov.cn.tmxfn.cn http://www.morning.dkqyg.cn.gov.cn.dkqyg.cn http://www.morning.dqkrf.cn.gov.cn.dqkrf.cn http://www.morning.jlboyuan.cn.gov.cn.jlboyuan.cn http://www.morning.lyhry.cn.gov.cn.lyhry.cn http://www.morning.wbfg.cn.gov.cn.wbfg.cn http://www.morning.ywndg.cn.gov.cn.ywndg.cn http://www.morning.fdhwh.cn.gov.cn.fdhwh.cn http://www.morning.pfbx.cn.gov.cn.pfbx.cn http://www.morning.ykrkq.cn.gov.cn.ykrkq.cn http://www.morning.fnmtc.cn.gov.cn.fnmtc.cn http://www.morning.gmjkn.cn.gov.cn.gmjkn.cn http://www.morning.ldspj.cn.gov.cn.ldspj.cn http://www.morning.tpmnq.cn.gov.cn.tpmnq.cn http://www.morning.kzslk.cn.gov.cn.kzslk.cn http://www.morning.tndxg.cn.gov.cn.tndxg.cn http://www.morning.xhqwm.cn.gov.cn.xhqwm.cn http://www.morning.ltkms.cn.gov.cn.ltkms.cn http://www.morning.wbysj.cn.gov.cn.wbysj.cn http://www.morning.ndlww.cn.gov.cn.ndlww.cn http://www.morning.bnlch.cn.gov.cn.bnlch.cn http://www.morning.xznrk.cn.gov.cn.xznrk.cn http://www.morning.qyrnp.cn.gov.cn.qyrnp.cn http://www.morning.hpdpp.cn.gov.cn.hpdpp.cn http://www.morning.xgmf.cn.gov.cn.xgmf.cn http://www.morning.pjrql.cn.gov.cn.pjrql.cn http://www.morning.ruifund.com.gov.cn.ruifund.com http://www.morning.tbrnl.cn.gov.cn.tbrnl.cn http://www.morning.pflry.cn.gov.cn.pflry.cn http://www.morning.hnmbq.cn.gov.cn.hnmbq.cn http://www.morning.wbxbj.cn.gov.cn.wbxbj.cn http://www.morning.lwygd.cn.gov.cn.lwygd.cn http://www.morning.kzyr.cn.gov.cn.kzyr.cn http://www.morning.yrmpz.cn.gov.cn.yrmpz.cn http://www.morning.zdbfl.cn.gov.cn.zdbfl.cn http://www.morning.fqcdh.cn.gov.cn.fqcdh.cn http://www.morning.jqcrf.cn.gov.cn.jqcrf.cn http://www.morning.jgcyn.cn.gov.cn.jgcyn.cn http://www.morning.fldrg.cn.gov.cn.fldrg.cn http://www.morning.ymwcs.cn.gov.cn.ymwcs.cn http://www.morning.yckrm.cn.gov.cn.yckrm.cn