• golang 库之「依赖注入」


    1. 写在最前面

    同事在技术分享的时候用了 golang 的 fx 框架,突然想起之前有一次帮别人 review 代码的时候也看到过这个框架。只是大概知道是「依赖注入」的框架,并没有深入分析理解过,包括这个框架的优势、劣势以及适合的场景一概不清楚。知识这个东西,摆在那里就是人家的,学到了才是自己的。所以顺便整理一波,方便后面自己要用的时候,可以很快的上手。

    注: 有再一再二,没有再三再四。第一次和第二次看到的时候可以说不理解,但是都第三次和第四次了,这可说不过去了。

    2. 依赖注入

    2.1 使用场景

    维基百科官方话术:「依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端)。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态一部分。传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。」

    注:维基百科的这个概念我读了三遍能理解,但是很难深入的分辨出什么场景适合用依赖注入。

    这个概念让我陷入了沉思,然后我又不死心的,问了问 chatgpt

    chatgpt 的回答如下:

    在这里插入图片描述

    总结:chatgpt 的回答整体上比维基百科理解起来要更简单一点。(ps:遇到不懂的概念可以试试 chatgpt

    2.2 框架对比

    以下是几个常用的 golang 依赖注入框架

    注:两者的差别,在于 wire 是使用 Code Gen 的方式,而 fx 则是使用的 reflection.

    3. fx 框架使用场景示例

    在使用框架之前,一定要先想清楚,业务的复杂程度真的到了必需框架不可的程度了吗?

    个人观点:

    • 引入库会一定程度上减少了业务开发的复杂性,但是也会导致理解代码的成本变高(ps: 通用的常见库除外,比如日志库

    3.1 示例

    实现一个 http server ,支持如下两个 POST 方法

    • /echo :将请求的内容,直接作为响应的 body

    • /hello:请求的内容拼接 hello 字符,将其作为响应的 body

    形如:

    在这里插入图片描述

    3.2 golang 原生的库

    代码:

    package main
    
    import (
        "fmt"
        "io"
        "log"
        "net/http"
    )
    
    var handleMap = map[string]func(w http.ResponseWriter, r *http.Request){
        "/echo":  handleEcho,
        "/hello": handleHello,
    }
    
    func main() {
        http.HandleFunc("/", handleRequest)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    func handleRequest(w http.ResponseWriter, r *http.Request) {
        f := handleMap[r.URL.Path]
        if f != nil {
            f(w, r)
            return
        }
    
        http.NotFound(w, r)
    }
    
    func handleEcho(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "Error reading request body", http.StatusInternalServerError)
            return
        }
    
        fmt.Fprintf(w, "%s", body)
    }
    
    func handleHello(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "Error reading request body", http.StatusInternalServerError)
            return
        }
    
        msg := fmt.Sprintf("%s hello", body)
        fmt.Fprint(w, msg)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    3.3 fx 库

    代码:

    package main
    
    import (
        "context"
        "fmt"
        "io"
        "net"
        "net/http"
    
        "go.uber.org/fx"
        "go.uber.org/zap"
    )
    
    // 定义的接口
    type Route interface {
        http.Handler
    
        Pattern() string
    }
    
    func AsRoute(f any) any {
        return fx.Annotate(
            f,
            fx.As(new(Route)),
            fx.ResultTags(`group:"routes"`),
        )
    }
    
    // 实现 Route 接口的数据结构, echo 接口
    type EchoHandler struct {
        log *zap.Logger
    }
    
    func NewEchoHandler(log *zap.Logger) *EchoHandler {
        return &EchoHandler{
            log: log,
        }
    }
    func (*EchoHandler) Pattern() string {
        return "/echo"
    }
    
    func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if _, err := io.Copy(w, r.Body); err != nil {
            h.log.Warn("Failed to handle request", zap.Error(err))
    
        }
    }
    
    // 实现 Route 接口的数据结构, hello 接口
    type HelloHandler struct {
        log *zap.Logger
    }
    
    func NewHelloHandler(log *zap.Logger) *HelloHandler {
        return &HelloHandler{log: log}
    }
    
    func (*HelloHandler) Pattern() string {
        return "/hello"
    }
    
    func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            h.log.Error("Failed to read request", zap.Error(err))
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
    
        if _, err := fmt.Fprintf(w, "Hello, %s\n", body); err != nil {
            h.log.Error("Failed to write response", zap.Error(err))
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
    }
    
    func NewServeMux(route []Route) *http.ServeMux {
        mux := http.NewServeMux()
        for _, r := range route {
            mux.Handle(r.Pattern(), r)
        }
        return mux
    }
    
    func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server {
        srv := &http.Server{Addr: ":8081", Handler: mux}
        lc.Append(fx.Hook{
            OnStart: func(ctx context.Context) error {
                ln, err := net.Listen("tcp", srv.Addr)
                if err != nil {
                    return err
                }
                log.Info("Starting HTTP Server", zap.String("addr", srv.Addr))
                go srv.Serve(ln)
                return nil
            },
            OnStop: func(ctx context.Context) error {
                return srv.Shutdown(ctx)
            },
        })
        return srv
    }
    
    func UseHttpServer(*http.Server) {}
    
    func main() {
        fx.New(
            fx.Provide(
                NewHTTPServer,
                fx.Annotate(
                    NewServeMux,
                    fx.ParamTags(`group:"routes"`),
                ),
                AsRoute(NewEchoHandler),
                AsRoute(NewHelloHandler),
                zap.NewExample,
            ),
            fx.Invoke(UseHttpServer),
        ).Run()
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    3.4 对比

    fx 库的官方介绍如下:

    • Eliminate globals: Fx helps you remove global state from your application. No more init() or global variables. Use Fx-managed singletons.
    • Code reuse: Fx lets teams within your organization build loosely-coupled and well-integrated shareable components.
    • Battle tested: Fx is the backbone of nearly all Go services at Uber.

    总结:

    • 消除了 init() 和 全局变量的使用

    • 解耦程度更好以及更方便的共享组件

    • 在 Uber 内部成熟度很高,几乎所有的 go 服务的底层支柱。

    但是:

    • 使用了 init() 和 全局变量 真的有什么坏处吗?

    • 你的程序真的需要那么高的解耦程度吗?

    • 「backbone」代表 100% 可靠吗?

    最近开发心得,代码凡是依赖别人的,交付时间完全依赖别人排期。但是代码全部控制在自己手里,那里方便修复改哪里!

    「你的修复方式,不会影响到客户。但是你为了追求完美的代码,延误了交付时间,一定会招来客户投诉」(ps:不要问我怎么知道的,成长血泪史

    3.4.1 如上两种实现方式对比
    golang 原生库fx 库
    开发复杂度
    解耦程度
    运维成本

    注:代码首先是需要人理解和维护的,其次都是其次。如果理解的成本变高,那相应的维护成本可想而知。

        以上等级划分:均为高中低三档
    
        按照笔者的比较:golang 原始库完胜 fx 库
    
    • 1
    • 2
    • 3

    注意:不能大而全的总结为 fx 库不好,而是总结为在代码的规模较少、又要保证交付节奏时,不引入复杂或者自己不熟悉的库,不失为一种很好的选择。

    注:凡是自己控制的,都是可靠的,凡是有依赖的,皆需要多问问「真的是这样吗?」

    3.4.2 关于过度设计

    上面的对比让笔者脑子中产生了一个之前看过的关键词「过度设计」。

    维基百科给出的定义:

    「过度设计」指的是一种过于复杂的方式设计产品或提供问题的解决方案的行为,而在这种情况下,可以证明存在一种更简单的解决方案,其效率和效果与原设计相同。

    注:参考 Paweł Głogowski 的这个定义,可以总计为「解决你所有没有的问题的代码和设计」

    原因: 我们试图预测未来,对未知的问题做准备。(ps:你认为的未来的问题,可能根本没有出现的机会,所以减少焦虑,不要过度,因为未来的事情担心和设计

    解决方法

    • 让工程师成为真正的产品工程师

    • 正确定义问题来减少模糊性

    • 多问:这对解决当前用户的问题有什么帮助?要是现在不解决会怎么?

    3.4.3 感悟
    • 开发者应该视自己的方案来选择技术实践,框架提供的是「选择」,而不是限制开发者的自由。

    • 框架的目的是协助工程师,如果不知道需要什么协助,用框架也帮不上忙,说不定还会束手束脚。

    4. 碎碎念

    上海的天气真的是瞬息万变,昨天还可以穿小裙子,今天就要穿呢子大衣,惹不起!

    • 时间扑面而来,我们终将释怀。健康的活着,平静的过着,开心的笑着,适当的忙着,就很好。

    • 第一是做让自己开心的事情,第二是「绝对不做让自己不开心的事情」。

    • 就算失败,我也想知道,自己倒在距离终点多远的地方。

    5. 参考资料

  • 相关阅读:
    LangChain介绍及代码实践
    [极客大挑战 2019]FinalSQL(bypass盲注)
    C语言:用一级指针访问二维数组的max,min
    SqlBoy:异或、交换奇偶
    径向基神经网络RBF:Matlab实现RBF神经网络(含例子及代码)
    alibaba国际版阿里巴巴API接入说明(阿里巴巴商品详情+关键词搜索商品列表)
    Web端即时通讯技术:WebSocket、socket.io、SSE
    AUTOCAD——文字显示方式、CAD怎么直接打开天正图纸
    python 视频抽帧
    Tomcat配置域名和端口
  • 原文地址:https://blog.csdn.net/phantom_111/article/details/134340507