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

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

  •  
  •   guonaihong ·
    guonaihong · 2019-10-16 09:29:51 +08:00 · 4831 次点击
    这是一个创建于 1625 天前的主题,其中的信息可能已经有所发展或是发生改变。

    昨天晚上快速看了 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 条附言  ·  2019-10-16 13:49:35 +08:00

    接着聊早上的内容

    原始需求,设计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
    flybird
        1
    flybird  
       2019-10-16 09:38:16 +08:00
    为啥我感觉第一种 bad code 更好呢,简单明了?
    下面 啰啰嗦嗦 实现了一堆,道理上讲貌似更好,读起来太啰嗦。
    dyllen
        2
    dyllen  
       2019-10-16 09:39:15 +08:00
    @flybird 估计这就是优雅吧,看起来高大上。上面的简单粗暴,像个粗人。
    ylsc633
        3
    ylsc633  
       2019-10-16 09:53:32 +08:00
    http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

    我曾在这看到的.. 就学习(模仿)了一下.. 很装逼... 哈哈哈
    reus
        4
    reus  
       2019-10-16 09:59:24 +08:00
    初期只有一两个参数,那当然第一种好

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

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

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

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

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

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