首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  程序员

golang: 从 Uber Go 风格指南,摄取 API 设计营养

  •  
  •   guonaihong · 27 天前 · 2916 次点击

    昨天晚上快速看了 uber go 风格指南,发现最后一条技巧,对 API 设计有帮助,拿出来大家一起讨论下。

    回顾

    bad code

    // package db
    
    func Connect(
      addr string,
      timeout time.Duration,
      caching bool,
    ) (*Connection, error) {
      // ...
    }
    
    // Timeout and caching must always be provided,
    // even if the user wants to use the default.
    
    db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
    db.Connect(addr, newTimeout, db.DefaultCaching)
    db.Connect(addr, db.DefaultTimeout, false /* caching */)
    db.Connect(addr, newTimeout, false /* caching */)
    

    good code

    type options struct {
      timeout time.Duration
      caching bool
    }
    
    // Option overrides behavior of Connect.
    type Option interface {
      apply(*options)
    }
    
    type optionFunc func(*options)
    
    func (f optionFunc) apply(o *options) {
      f(o)
    }
    
    func WithTimeout(t time.Duration) Option {
      return optionFunc(func(o *options) {
        o.timeout = t
      })
    }
    
    func WithCaching(cache bool) Option {
      return optionFunc(func(o *options) {
        o.caching = cache
      })
    }
    
    // Connect creates a connection.
    func Connect(
      addr string,
      opts ...Option,
    ) (*Connection, error) {
      options := options{
        timeout: defaultTimeout,
        caching: defaultCaching,
      }
    
      for _, o := range opts {
        o.apply(&options)
      }
    
      // ...
    }
    
    // Options must be provided only if needed.
    
    db.Connect(addr)
    db.Connect(addr, db.WithTimeout(newTimeout))
    db.Connect(addr, db.WithCaching(false))
    db.Connect(
      addr,
      db.WithCaching(false),
      db.WithTimeout(newTimeout),
    )
    

    技巧肢解

    里面主要用了两种技巧

    • 可变长参数
    • 函数(或者接口)当配置

    可变长参数的好处

    gout https://github.com/guonaihong/gout

    gout(流式 http client) 可以使用 SetCookies 可以设置一个或者多个 cookie。在大多数开源库里面用了两个函数实现类似功能。gout 这里用上可变长参数可以减少 API 个数。

    gin 里面

    在 gin(API 框架) Run 函数就是可变长参数经典用法,你可以用 router.Run()起个默认服务,也可以用 router.Run(":1234") 指定端口起服务。这里也可以减少 API 个数,写起来很爽。

    上面举的例子可以归纳出,可变长参数用在,函数参数个数 >=0 的地方,很爽。

    函数(接口)当配置

    //TODO,中午再补上。

    第 1 条附言  ·  27 天前

    接着聊早上的内容

    原始需求,设计Debug函数,输出日志

    聊函数当配置,先聊假如要设计一个debug函数,目的是输出日志。第一反应是设计如下API

    // 函数原型
    func Debug(flag bool) {}
    
    // 使用
    Debug(true)
    

    加需求,支持重定向输出源

    并且输出源不是必须的,上面聊过,支持>=0个参数,知道用可变长参数。 如果类型都不一样,可以用interface{} ok,基于这两点认知修改函数,函数内部用 类型断言或者反射可区分出类型

    // 函数原型
    func Debug(x ...interface{}){}
    
    // 使用
    var w bytes.Buffer{}
    Debug(true, &w)
    

    支持颜色高亮

    type OpenColor bool
    
    Debug(true, OpenColor(true))
    

    加需求,支持环境变量打开日志

    这时候需要上函数,

    func iosDebug() bool {
        len(os.Getenv("IOS_DEBUG")> 0
    }
    
    // 使用
    Debug(true, iosDebug)
    

    加需求,组合使用,发现问题

    有环境变量IOS_DEBUG打开日志,并且支持颜色高亮。假如要修改呢? 几百几千个日志调用的地方都要修改。。。

    // 使用
    Debug(true, iosDebug, OpenColor(true))
    

    我们优化下上面的做法

    优化,减少改动带来的影响

    type debugOption struct {
        openOutput bool
        openColor bool
        w io.Write
    }
    
    // Option overrides behavior of Connect.
    type Option interface {
      apply(*debugOption)
    }
    
    type optionFunc func(*debugOption)
    
    func (f optionFunc) apply(o *debugOption) {
      f(o)
    }
    
    func IOSDebugOpen() Option {
      return optionFunc(func(o *debugOption) {
        if len(os.Getenv("IOS_DEBUG")> 0 {
             o.openOutput = t
             o.openColor =true
        }
      })
    }
    
    // 函数定义
    // 函数内部只要执行apply接口就行
    func Debug(x ...interface{}){}
    
    // 使用
    Debug(IOSDebugOpen())
    
    

    如果有全局修改只要改IOSDebugOpen函数就行。

    最后细节优化

    • 可以提供Debug(IOSDebugOpen()) 和Debug(true)两种用法,内部用reflect区分出来即可
    • 针对常用组合提供预制函数,方便使用

    收获

    得到一个用起来不错的Debug函数。它拥有很强的组合功能,还方便修改。通过环境变量,可以拥有namespace级别的日志输出。

    总结

    我们通过一步一步的变化得到一个很灵活的代码套路。它如此强大。 什么时候要用它,对API设计有很高的要求,追求美感,不计较时间。平时该咋地 咋地,哈哈。。。

    github

    https://github.com/guonaihong/gout

    30 回复  |  直到 2019-11-10 00:32:15 +08:00
        1
    flybird   27 天前
    为啥我感觉第一种 bad code 更好呢,简单明了?
    下面 啰啰嗦嗦 实现了一堆,道理上讲貌似更好,读起来太啰嗦。
        2
    dyllen   27 天前
    @flybird 估计这就是优雅吧,看起来高大上。上面的简单粗暴,像个粗人。
        3
    ylsc633   27 天前
    http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

    我曾在这看到的.. 就学习(模仿)了一下.. 很装逼... 哈哈哈
        4
    reus   27 天前
    初期只有一两个参数,那当然第一种好

    后期参数多了,就直接加 Option 结构体

    用函数这种……没有十几二十个选项我是不会用的
        5
    reus   27 天前   ♥ 1
    著名 go 开发者 Dave Cheney 写过相关博文: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
        6
    zunceng   27 天前
    挺好的

    不过用了依赖注入以后这样就不好写了
        7
    reus   27 天前
    @zunceng 没错,用了依赖注入,第一种反而是最方便注入的
        8
    scnace   27 天前 via Android
    @reus 刚想发
        9
    zzlettle   27 天前
    个人感觉,go 的这种函数变接口的方式,不要到处用
    能用简单直接,人类思维方式的语言来实现的,就用我们人能一眼看明白的方式来写代码
    他这种写法,几乎一半以上实在炫技
    就好像,同样是吃饭,用什么碗都可以,有钱人非要用黄金碗
    其实重要的是你碗里面的东西

    唯一的作用就是吓唬不懂得人
    反人类
    这就是为什么 python 运行效率不高,但是热度慢慢攀升到第一得原因
    因为他是人类得语言
        10
    love   27 天前
    第二个看着太吓人了,在别的语言比如 Typescript 里简单明了且也有类型保证的传参方式在 Go 里怎么这么反人类。

    看来写普通业务用 Go 实在不是一个好选择。
        11
    maichael   27 天前
    其实要看你的 API 面向谁开发。
        12
    zzlettle   27 天前   ♥ 1
    说句现在央视里面常用得政治术语来形容
    go 的这套语法风格
    跟 python 的语法风格
    区别就好像
    是党指挥枪,还是枪指挥党
    python 就是人类思维
    是党指挥枪
    所有方法函数,要围绕数据来运行
    go 就是枪指挥党
    所有方法函数,指挥其他的数据变量
    人类天生的思维方式就是
    数据驱动
    就是我学习本领,掌握了方法
    让方法为我而用
    现在 go 的这套
    就是我们人类围绕方法而改变自己

    go 的这套语法真的不太适合初学者
        13
    wingoo   27 天前
    functional options 对于调用者友好, 开发者不友好
    不过实现好一个单独的类库还是挺好用的
        14
    Mark3K   27 天前
    grpc 中用了大量的这类技巧,看看就能学会
        15
    chenqh   27 天前 via Android
    这么喜欢封装,不写 java 可惜了
        16
    optional   27 天前
    db.WithCaching ... 要用 builder 就彻底一点 db.ConnectionBuilder().Withxxx().Withxxx().connect();
        17
    chendy   27 天前
    看不太明白的 java 程序员表示:何不 Builder ?
        18
    gfreezy   27 天前
    为啥不用 Builder,好像更加简单直观,功能也更强大
        19
    wangxiaoaer   27 天前
    这个跟 js 的传入一个 options 对象作为参数差不多吧,但是代码看起来想死,尤其是那种隐式的接口实现。
        20
    clippit   27 天前
    @optional golang 里不兴链式调用
        21
    useben   27 天前
    函数选项模式呗,go-micro 的插件化就是基于此模式的。
    婉转的实现了可变参数和默认参数的目的
        22
    zjsxwc   27 天前 via Android
    这种需求还不如用 builder 模式来得简单易懂
        23
    zjh6   27 天前
    golang 是谷哥搞的,就要晓得其没前途了.
    人对了,什么都是对的.
    人错了,怎么走都是错!
        24
    guonaihong   27 天前
    @Mark3K 是吗?有时间玩下。
        25
    yixinlove   27 天前
    我觉得还是看什么时候吧,如果配置项太多,可以考虑第二种,如果配置项只有那么几个,就没必要了。
    一切还是以人为本写代码,太复杂会看的头晕。
        26
    guonaihong   26 天前
    @zjh6 go 代码是开源的。问题不大,真的发生 google 不维护,也会有社区维护的,现在可以修改编译器源码的童鞋已经不少了。
        27
    guonaihong   26 天前
    @love 可否推荐个开发普通业务不错的语言,以后玩下。
        28
    love   26 天前
    @guonaihong 我用的是 js,当然 V2 有大把莫名奇妙的 java 码农疯狂 diss nodejs,你看着办
        29
    guonaihong   24 天前
    @love js 是挺不错的语言。我后面也打算玩下。
        30
    zhixuanziben   3 天前
    @love nodejs 出活挺快的,加上 ts 也有类型系统了,做做 CRUD 还是很方便的
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1055 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 24ms · UTC 18:54 · PVG 02:54 · LAX 10:54 · JFK 13:54
    ♥ Do have faith in what you're doing.