Go语言中的可选参数?

761

Go语言是否支持可选参数?还是我可以定义两个不同参数数量的同名函数?


89
谷歌做出了一个糟糕的决定,因为有时一个函数的使用情况会有90%的情况和10%的情况。可选参数就是为了那10%的情况而设计的。合理的默认值意味着更少的代码,更少的代码意味着更易维护。 - Jonathan
6
我认为不提供可选参数是一个明智的决定。我曾经看到在 C++ 中滥用可选参数 -- 超过 40 个参数。如果没有命名参数,通过逐一计数参数并确保指定正确的参数非常容易出错。像 @deamon 所提到的那样,使用结构体会更好。 - jnnnnn
5
@Jonathan,处理这个问题有几种方法。其中一种方法是传递一个包含所有函数参数的结构体。这样做的好处是具有命名参数(比位置参数更清晰),所有未提供的参数都具有其默认值。当然,也可以创建一个包装函数,将默认值传递给完整的函数。例如Query和QueryWithContext。 - Falco
2
现代IDE从语义中自动生成命名参数,因此让程序员重新设计他们的代码来做一些IDE可以为您完成的事情似乎不是最理想的选择。 - Jonathan
2
@Jonathan,它似乎在VS Code、Visual Studio、IntelliJ、Atom或Sublime中不能直接使用。你指的是哪个IDE,还是有扩展/设置可以提供这个功能? - Falco
显示剩余3条评论
15个回答

664

Go语言没有可选参数也不支持方法重载

如果方法调度不需要进行类型匹配,它将变得更加简单。从其他编程语言的经验来看,使用相同名称但具有不同签名的各种方法有时很有用,但实践中也可能会导致混乱和脆弱性。只通过名称进行匹配并要求类型一致是Go类型系统中一个重要的简化决策。


86
那么,make是一个特例吗?还是它甚至并没有真正实现为一个函数... - mk12
91
@Mk12 make 是一种语言结构,上面提到的规则不适用。请参见这个相关问题 - nemo
25
方法重载 - 这在理论上是一个很好的想法,如果实现得当也会非常出色。然而,在实践中,我见过一些无用且难以理解的重载,因此我同意谷歌的决定。 - trevorgk
342
我要冒险表示不同意这个选择。语言设计者基本上是在说:“我们需要函数重载来设计我们想要的语言,所以make、range等函数本质上是被重载的,但如果你想要函数重载来设计你想要的API,那就很难了。”一些程序员滥用语言特性并不是废除该特性的理由。 - Tom
20
我需要在这里宣泄一下我的愤怒 - 这没有意义,但对保持理智很有用。方法重载和可选参数可以并且已经被误用以产生难以阅读的混乱,这并不是不实现它们的有效理由。 - Marcello Romani
显示剩余14条评论

344

实现类似于可选参数的一个好方法是使用变长参数。函数实际上接收到的是你指定类型的切片。

func foo(params ...int) {
    fmt.Println(len(params))
}

func main() {
    foo()
    foo(1)
    foo(1,2,3)
}

8
在上面的例子中,params 是一个整数切片。 - Ferguzz
131
但仅适用于相同类型的参数 :( - Juan de Parras
26
嗯,我想你仍然可以使用类似...interface{}的东西。 - maufl
11
使用 ...type 无法传达各个选项的含义。建议使用结构体代替。...type 适用于需要在调用前放入数组中的值。 - user3523091
11
这让我感觉到完美的语言并不存在。我喜欢 Go 的所有东西,除了这个问题。 - Yash Kumar Verma
显示剩余3条评论

239

您可以使用一个包含参数的结构体:

type Params struct {
  a, b, c int
}

func doIt(p Params) int {
  return p.a + p.b + p.c 
}

// you can call it without specifying all parameters
doIt(Params{a: 1, c: 9})

与省略号(params ...SomeType)相比,主要优势在于您可以使用参数结构体(struct)来处理不同类型的参数。


18
如果结构体能在此处具有默认值就好了;用户省略的任何内容都将默认为该类型的nil值,这可能是或可能不是函数的适当默认参数。 - jsdw
55
@lytnus,我不想纠结于这个问题,但是如果某些字段的值被省略了,它们将默认为其类型的“零值”;而 nil 则是另一回事。如果省略的字段的类型恰好是指针,则其零值将为 nil。 - burfl
10
是的,除非是针对于不具有实际意义的类型(比如布尔型),否则“零值”的概念对于整型、浮点型和字符串类型没有任何用处,因为这些数值都具有意义。所以,你无法判断该结构体中是否存在相应字段,或者该字段的值是否被有意地设置成了零值。 - keymone
3
@keymone,我不反对你的观点。我只是关于上面的说法过于追求学究式而已,即用户省略的值默认为该类型的“nil值”,这是不正确的。它们将默认为零值,这可能是nil,也可能不是,具体取决于该类型是否为指针。 - burfl
6
我认为需要考虑并使用这种选项的事实表明,有可选和默认参数可能会更好。至少,如果我们有它们,那么目的就是清晰的,而不是被人工构造所掩盖,这些构造可能使开发者的意图变得模糊,并且可能被滥用超出其预期用途。 - Alan Carlyle

196

对于任意数量、可能很大的可选参数,一个好的习惯是使用函数选项

对于您的类型Foobar,首先只编写一个构造函数:

func NewFoobar(options ...func(*Foobar) error) (*Foobar, error){
  fb := &Foobar{}
  // ... (write initializations with default values)...
  for _, op := range options{
    err := op(fb)
    if err != nil {
      return nil, err
    }
  }
  return fb, nil
}

每个选项都是一个能够改变Foobar的函数。然后为用户提供方便的方法来使用或创建标准选项,例如:

func OptionReadonlyFlag(fb *Foobar) error {
  fb.mutable = false
  return nil
}

func OptionTemperature(t Celsius) func(*Foobar) error {
  return func(fb *Foobar) error {
    fb.temperature = t
    return nil
  }
}

游乐场

为了简洁起见,您可以给选项的类型(游乐场)命名:

type OptionFoobar func(*Foobar) error

如果您需要强制参数,请在变参选项之前将它们作为构造函数的第一个参数添加。
“功能选项”习语的主要优点是:
- 您的API可以随着时间的推移而增长,而不会破坏现有代码,因为当需要新选项时,构造函数签名保持不变。 - 它使默认用例最简单:根本没有参数! - 它提供了对复杂值初始化的精细控制。
这种技术是由Rob Pike首创,并由Dave Cheney演示。

8
好的,我会尽力为您进行翻译。以下是需要翻译的内容:来源:http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html,http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis这篇文章介绍了自指函数的概念和如何使用它们来提高代码的可读性和可维护性。通过将自指函数应用于代码中的重复模式,可以减少冗余,并使代码更具表现力和易于理解。本文还介绍了一种名为“函数选项”的技术,该技术可用于创建易于使用的API,并可在不破坏向后兼容性的情况下添加功能。函数选项可以更好地组织代码,并使其更具扩展性和可读性。作者强调了设计良好的API对于软件项目的成功至关重要,因此建议开发人员在编写API时采用这些技术来提高其质量和易用性。 - Deleplace
39
聪明,但太复杂了。Go 语言的哲学是用简单直接的方式编写代码。只需传递一个结构体并测试默认值。 - user3523091
22
FYI,这个成语的原作者至少在第一个引用出版物中是指Commander Rob Pike。我认为他在Go哲学方面有权威性。链接-https://commandcenter.blogspot.bg/2014/01/self-referential-functions-and-design.html。同时搜索“Simple is complicated”。 - Petar Donchev
早就点赞了,但你写道:为用户提供方便的方式来使用或创建标准选项。 如果 FooBar 是一个不透明的结构体类型,我不知道用户如何创建其他选项。如果 FooBar 不是不透明的,则函数选项的好处是无意义的,因为现在用户有两种初始化 FooBar 的方法:通过结构字面量或通过工厂函数(“构造函数”)。我的结论是,可选选项在选项集合被认为是封闭且由包作者控制时效果最佳。 - jub0bs
当你在同一个命名空间中同时拥有结构体Foo和Bar,它们都可以配置温度时会发生什么?那么你是否需要OptionTemperatureForFooOptionTemperatureForBar?这难道不会使构造函数变得像NewFoo(OptionTemperatureForFoo(100), OptionReadonlyFlagForFoo(), OptionOtherForFoo())这样难以理解吗?有什么方法可以改进这种情况吗? - undefined
显示剩余5条评论

24

12

你可以使用一个映射(map)来传递任意命名的参数。如果这些参数具有不同类型,则需要使用“aType = map[key].(*foo.type)”来断言类型。

type varArgs map[string]interface{}

func myFunc(args varArgs) {

    arg1 := "default"
    if val, ok := args["arg1"]; ok {
        arg1 = val.(string)
    }

    arg2 := 123
    if val, ok := args["arg2"]; ok {
        arg2 = val.(int)
    }

    fmt.Println(arg1, arg2)
}

func Test_test() {
    myFunc(varArgs{"arg1": "value", "arg2": 1234})
}

1
以下是对这种方法的一些评论:https://www.reddit.com/r/golang/comments/546g4z/namedoptional_function_parameters/ - Brent Bradburn
1
请点击以下链接查看:Go语言是否支持函数/方法重载? - Brent Bradburn
1
@ЯрославРахматуллин:这是一个教程,不是实时代码。有时候解释一下会更好。 - Brent Bradburn

11

根据面向C++程序员的Go文档,Go语言不支持函数重载和用户自定义操作符。

Go语言不支持函数重载,也不支持用户自定义操作符。

我找不到同样清晰的声明来表明可选参数不受支持,但它们也不被支持。


9
“目前没有针对这个[可选参数]的计划。” Ian Lance Taylor,Go语言团队。 http://groups.google.com/group/golang-nuts/msg/030e63e7e681fd3e - peterSO
1
不允许用户定义运算符是一个糟糕的决定,因为它是任何流畅的数学库的核心,例如线性代数中的点积或叉积,通常用于3D图形。 - Jonathan

9

我感觉我来晚了,但我在搜索是否有比我已经做的更好的方法。这种方法解决了您尝试解决的问题,并提供了可选参数的概念。

package main

import "fmt"

type FooOpts struct {
    // optional arguments
    Value string
}

func NewFoo(mandatory string) {
    NewFooWithOpts(mandatory, &FooOpts{})
}

func NewFooWithOpts(mandatory string, opts *FooOpts) {
    if (&opts) != nil {
        fmt.Println("Hello " + opts.Value)
    } else {
        fmt.Println("Hello")
    }
}

func main() {
    NewFoo("make it work please")

    NewFooWithOpts("Make it work please", &FooOpts{Value: " World"})
}

更新1:

添加了一个功能性示例,以展示功能与样例的区别


2
我喜欢这个选择胜过其他的。此外,这是我在许多库中看到的一种模式,当某些东西有不同的选项并且将被重复使用时,您可以创建一个结构来表示这些选项,并通过参数传递选项,或者您可以将选项设置为 nil 以使用默认值。此外,选项可以在它们自己的结构中进行文档化,并且您可以创建预定义的选项集。我在 GitHub 客户端库和 go-cache 库等其他库中看到了这一点。 - theist
1
@madzohan请不要更改我的代码示例以适应您的需求...您可以请求进行更改或提供自己的示例...您的示例从根本上改变了我的示例功能。一个执行某些操作的void函数不需要返回来适应您的需求。 - Maxs728

7

Go语言不支持可选参数、默认值和函数重载,但你可以使用一些技巧来实现相同的功能。

下面分享一个例子,可以在一个函数中使用不同数量和类型的参数。这是一段简单易懂的代码,需要添加错误处理和一些逻辑。

func student(StudentDetails ...interface{}) (name string, age int, area string) {
    age = 10 //Here Age and area are optional params set to default values
    area = "HillView Singapore"

    for index, val := range StudentDetails {
        switch index {
            case 0: //the first mandatory param
                name, _ = val.(string)
            case 1: // age is optional param
                age, _ = val.(int)
            case 2: //area is optional param
                area, _ = val.(string)
        }
    }
    return
}

func main() {
    fmt.Println(student("Aayansh"))
    fmt.Println(student("Aayansh", 11))
    fmt.Println(student("Aayansh", 15, "Bukit Gombak, Singapore"))
}

13
那太可怕了。 - glycoslave

6

如果您不想使用指针,可以使用它们并将其保留为nil:

func getPosts(limit *int) {
  if optParam != nil {
    // fetch posts with limit 
  } else {
    // fetch all posts
  }
}

func main() {
  // get Posts, limit by 2
  limit := 2
  getPosts(&limit)

  // get all posts
  getPosts(nil)
}

2
完全同意。有时将nil作为参数放入代码中比进行额外的更改要简单得多。 - Michael Yohanes
想看看是否可以使用可选参数或者参数默认值来实现这个功能; func (n *Note) save(extension string = ".txt") { ... } 将".txt"作为文件的默认扩展名,但是可以更改。然而,现在我开始意识到这并不是Go语言的设计理念,最好还是使用单独的Save()和SaveWithExtension(ext string)函数。与其抗拒它,这样做只会使一切变得更加困难。 - Alan Carlyle
除非您开始使用“iota”和“自动递增”常量,否则祝您好运处理不可寻址的常量(因为常量当然是魔法,没有内存地址)。 - Adonis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接