Go创建对象时,如何优雅的传递初始化参数

Go创建对象时,如何优雅的传递初始化参数?这里所说的优雅,指的是:

  1. 支持传递多个参数
  2. 参数个数、类型发生变化时,尽量保持接口的兼容性
  3. 参数支持默认值
  4. 具体的参数可根据调用方需关心的程度,决定是否提供默认值

Go并不像c++python那样,支持函数默认参数。所以使用Go时,我们需要一种方便、通用的手法来完成这件事。

Go的很多开源项目都使用Option模式,但各自的实现可能有些许细微差别。

本文将通过一个渐进式的demo示例来介绍Option模式,以及相关的一些思考。本文将内容切分为10个小模块,如果觉得前面的铺垫冗余,想直接看Option模式的介绍,可以从小标题七开始阅读。

先看demo,一开始我们的代码是这样的:

type Foo struct {
  num int
  str string

  // ...
}

func New(num int, str string) *Foo {
  // ...

  return &Foo{
    num: num,
    str: str,
  }
}

// ...

我们有一个Foo结构体,内部有numstr两个属性,New函数传入两个初始化参数,构造一个Foo对象。

ok,一切都足够简单。

假设我们需要对Foo内部增加两个属性,同时构造函数也需要支持传入这两个新增属性的初始值。有一种修改方法是这样的:

func New(num int, str string, num2 int, str2 string)

可以看到,这种方式,随着初始化参数个数、类型的变化,我们New函数的函数签名也需随之改变。这带来两个坏处:

  1. 对调用方来说,函数不兼容
  2. 参数数量太多,可读性可能变差

有一种保持兼容性的解决方案,是保留之前的New函数,再创建一个新的构造函数,比如New2,用于实现4个参数的构造方法。

这种解决方案在大部分时候会导致代码可维护性下降。

另一种解决方案,是把所有的参数都放入一个结构体中。就像这样:

type Foo struct {
  option Option

  // ...
}

type Option struct {
  num int
  str string
}

func New(option Option) *Foo {
  // ...

  return &Foo{
    option: option,
  }
}

这种方式,解决了上面提出的两个问题。但是,假设我们想为参数提供默认参数呢?

比如说当调用方不设置num时,我们希望它的默认值是100;不设置str时,默认值为hello

// 构造对象时只设置 str,不设置 num
foo := New(Option{
  str: "world",
})

这种做法可行的前提是,属性的默认值也为0值。

假设我们希望option.num属性默认值是100,那么当内部接收到的option.num0时,我们没法区分是调用方希望将option.num设置为0,还是调用方压根就没设置option.num。从而导致我们不知道将内部的option.num设置为0好,还是保持默认值100好。

事实上,这个问题不仅仅是传递Option时才会出现,即使所有参数都使用最上面那种直接传递的方式,也会存在这个问题,即0值无法作为外部是否设置的判断条件。

有一种解决方案,是使用*Option即指针类型作为初始化参数,如果外部传入为nil,则使用默认参数。代码如下:

func New(option *Option) *Foo {
  if option == nil {
    // 外部没有设置参数
  }
}

该方案存在的问题是,所有的参数要么全部由外部传入,要么全部使用默认值。

如何才能细化到每一个具体的参数,外部设置了使用外部设置的值,外部没有设置则使用默认值呢?

一种解决方案,是Option中的所有属性,都使用指针类型,如果特定参数为nil,则该参数使用默认参数。代码如下:

type Option struct {
  num *int
  str *string
}

func New(option Option) *Foo {
  if option.num == nil {
    // num 使用默认值
  } else {
    // option.num 即为调用方设置的初始值
  }
  // ...
}

该方案存在的问题是,对于调用方来说,使用起来有些反人类,因为你无法使用类似&1的写法对一个整型字面常量取地址,这意味着调用方必须格外定义一个变量保存他需要设置的参数的值,然后再对这个变量取地址赋值给Option的属性。代码如下:

// // 下面这种写法会造成编译错误
// option := {
//     num: &200,
//     str: &"world",
// }
//
// // 只能这样写
// num := 200
// str := "world"
// option := {
//     num: &num,
//     str: &str,
// }
// foo := New(option)

看起来有点,额,不太优雅。

另一种值得一提的解决方案,是使用Go可变参数的特性。代码如下:

func New(num int, str string, num2 ...int) {
  if len(num2) == 0 {
    // 调用方没有设置 num2,内部的 num2 应使用默认值
  } else {
    // num2[0] 即为调用方设置的初始值
  }
}

该方案存在的问题是,只能有一个参数有默认值。

ok,说了这么多,是时候开始上主菜了。Go是支持头等函数的语言,即可以将函数作为变量传递。所以我们可以像下面这样写:

type Option struct {
  num int
  str string
}

type ModOption func(option *Option)

func New(modOption ModOption) *Foo {
  // 默认值
  option := Option{
    num: 100,
    str: "hello",
  }

  modOption(&option)

  return &Foo{
    option: option,
  }
}

我们的New函数不再直接接收Option的值,而是提供了一种类似于钩子函数的功能,使得在内部对option设置完默认值之后,调用方可以直接选择修改哪些属性。比如调用方只设置num,代码如下:

New(func(option *Option) {
  // 调用方只设置 num
  option.num = 200
})

那么假设有些时候,我们觉得某个参数是调用方必须关心的,不应该由内部设置默认值呢?我们可以这样写:

package main

type Foo struct {
  key string
  option Option

  // ...
}

type Option struct {
  num  int
  str  string
}

type ModOption func(option *Option)

func New(key string, modOption ModOption) *Foo {
  option := Option{
    num: 100,
    str: "hello",
  }

  modOption(&option)

  return &Foo{
    key: key,
    option: option,
  }
}

// ...

func main() {
  New("iamkey", func(option *Option) {
    // 调用方只设置 num
    option.num = 200
  })
}

最后再来一种常见的、高级点的写法。在上面代码的基础上,增加如下代码:

func WithNum(num int) ModOption {
  return func(option *Option) {
    option.num = num
  }
}

func WithStr(str string) ModOption {
  return func(option *Option) {
    option.str = str
  }
}

然后是调用方的代码:

// 可以这样写
foo := New("iamkey", WithNum(200))
// 还可以这样写
foo := New("iamkey", WithStr("world"))

能不能两个一起用呢?其实是可以的,结合我们上文讲到的可变参数,将New函数修改如下:

func New(key string, modOptions ...ModOption) *Foo {
  option := Option{
    num: 100,
    str: "hello",
  }

  for _, fn := range modOptions {
    fn(&option)
  }

  return &Foo{
    key: key,
    option: option,
  }
}

然后是使用方的代码:

New("iamkey", WithNum(200), WithStr("world"))

总结

至此,关于Option模式的介绍就结束啦。

事实上,Option模式除了在创建对象时可以使用,里面的一些API设计思想,Go的小技巧,在编写普通函数时也可以使用。

模式说白了就是一种套路。在实现功能的基础之上,大家都熟悉了某种固有套路的写法,都按着这个套路走,那么代码的可读性、可维护性就更高些。

对于一个特定场景,没有最好的模式,只有最适合的模式。不要过度设计,手里就一把锤子,瞅啥都是钉子。

举个例子,最后说的那种WithXXX写法,我个人认为在大部分时候都有点皮裤套棉裤,简单事情复杂化的感觉,不如只用一个ModOption直接修改option来得简单、直观,所以我几乎不用WithXXX的写法。但是在有些场景,你如果觉得提供WithXXX对调用方更友好,那么用用也挺好。

为了保持场景的纯粹性,上面的demo可能会有些抽象。如果你想进一步看看Option模式在实际项目中是如何使用的,可以看看我的这个开源项目:naza。该项目在构造对象时大量使用了Option模式。比如 consistenthash.gobitrate.go 等等。并且做了一些私人化的风格规范。

最后,感谢阅读,如果觉得文章还不错,可以给我的github项目naza 来个star哈。该项目是我学习Go时写的一些轮子代码集合,后续我还会写一些文章逐个介绍里面的轮子以及一些写Go代码的技巧。

naza项目地址: https://github.com/q191201771/naza

naza的其他的文章:

原文链接: https://pengrl.com/p/60015/
原文出处: yoko blog (https://pengrl.com)
原文作者: yoko
版权声明: 本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。

本篇文章由一文多发平台ArtiPub自动发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,360评论 0 5
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,140评论 0 9
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,095评论 0 21
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,714评论 0 38