【问题标题】:Optional Parameters in Go?Go中的可选参数?
【发布时间】:2011-01-03 04:19:28
【问题描述】:

Go 可以有可选参数吗?或者我可以只定义两个具有相同名称和不同数量参数的函数吗?

【问题讨论】:

  • 相关:这是在使用可变参数作为可选参数时强制强制参数的方法:Is it possible to trigger compile time error with custom library in golang?
  • Google 做出了一个糟糕的决定,因为有时一个函数有 90% 的用例,然后是 10% 的用例。可选的 arg 用于 10% 的用例。合理的默认值意味着更少的代码,更少的代码意味着更高的可维护性。
  • 我认为没有可选参数是一个不错的决定。我已经看到可选参数在 C++ 中被严重滥用——40 多个参数。计算参数并确保指定正确的参数非常容易出错,尤其是在没有命名参数的情况下。使用@deamon 提到的结构要好得多。
  • @Jonathan 有几种方法可以解决这个问题。一种方法是传递一个带有函数所有参数的结构。这将具有具有命名参数(比位置参数更清晰)的额外好处,并且所有未提供的参数都有其默认值。当然只是创建一个包装函数,它将默认值传递给完整的函数。例如查询和QueryWithContext
  • @Jonathan 它似乎在 VS Code、Visual Studio、IntelliJ、atom 或 sublime 中无法开箱即用。您指的是什么 IDE,或者是否有提供此功能的扩展/设置?

标签: go overloading


【解决方案1】:
推荐的答案 Go Language

Go 没有可选参数nor does it support method overloading:

方法分派被简化,如果它 不需要进行类型匹配 好吧。其他语言的经验 告诉我们有各种各样的 方法名称相同但 偶尔会有不同的签名 有用,但它也可能是 在实践中令人困惑和脆弱。 仅按名称匹配并要求 类型的一致性是一个主要的 简化 Go 类型的决策 系统。

【讨论】:

  • 那么make 是一个特例吗?或者它甚至没有真正实现为一个函数……
  • @Mk12 make 是一种语言结构,上述规则不适用。见this related question
  • 方法重载 - 理论上的好主意,如果实施得当则非常出色。但是,我在实践中目睹了难以理解的垃圾超载,因此我同意 Google 的决定
  • 我会四处走动,不同意这个选择。语言设计者基本上说过,“我们需要函数重载来设计我们想要的语言,所以 make、range 等本质上都是重载的,但是如果你想通过函数重载来设计你想要的 API,那就很难了。”一些程序员滥用语言特性这一事实并不是摆脱该特性的理由。
  • @Tom 他们认为函数重载是滥用但 goto 的就好了... (╯°□°)╯︵ ┻━┻
【解决方案2】:

实现可选参数之类的好方法是使用可变参数。该函数实际上接收您指定的任何类型的切片。

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

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

【讨论】:

  • 在上面的例子中,params 是一个整数切片
  • 但只适用于同类型的参数:(
  • @JuandeParras 好吧,我猜你仍然可以使用 ...interface{} 之类的东西。
  • 使用 ...type 您并没有传达各个选项的含义。请改用结构。 ...type 对于在调用之前必须放入数组中的值很方便。
  • 这让我觉得完美的语言并不存在。喜欢一切,但是这个:(
【解决方案3】:

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

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})

【讨论】:

  • 如果结构在这里可以有默认值就好了;用户省略的任何内容都默认为该类型的 nil 值,这可能是也可能不是函数的合适默认参数。
  • @lytnus,我讨厌分裂头发,但是省略值的字段将默认为其类型的“零值”; nil 是另一种动物。如果省略字段的类型恰好是指针,则零值将为 nil。
  • @burfl 是的,除了“零值”的概念对于 int/float/string 类型绝对没用,因为这些值是有意义的,因此如果省略该值,您将无法区分从结构中或者如果有意传递零值。
  • @keymone,我不同意你的观点。我只是对上面的陈述表示迂腐,即用户省略的值默认为“该类型的 nil 值”,这是不正确的。它们默认为零值,可能为零也可能不为零,具体取决于类型是否为指针。
  • 我觉得像这样的选项需要考虑并且可以使用这一事实突出表明具有可选和默认参数可能会更好。至少,如果我们拥有它们,那么目的就很明确了,而不是隐藏在人为的构造背后,这些构造掩盖了开发人员的意图,而这些构造本身最终可能被滥用,超出了他们的预期。
【解决方案4】:

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

对于你的类型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
  }
}

Playground

为简洁起见,您可以为选项的类型命名(Playground):

type OptionFoobar func(*Foobar) error

如果您需要强制参数,请将它们作为构造函数的第一个参数添加到可变参数 options 之前。

Functional options 习惯用法的主要好处是:

  • 您的 API 可以随着时间的推移而增长,而不会破坏现有代码,因为在需要新选项时构造函数签名保持不变。
  • 它使默认用例变得最简单:根本没有参数!
  • 它提供对复杂值初始化的精细控制。

Rob Pike 创造了这项技术,Dave Cheney 也演示了这种技术。

【讨论】:

  • 聪明,但太复杂了。 Go 的哲学是以直接的方式编写代码。只需传递一个结构并测试默认值。
  • 仅供参考,这个成语的原作者,至少是第一个被引用的出版商,是指挥官 Rob Pike,我认为他对围棋哲学具有足够的权威性。链接-commandcenter.blogspot.bg/2014/01/…。同时搜索“简单就是复杂”。
  • #JMTCW,但我发现这种方法很难推理。我更愿意传入一个值结构,如果需要,其属性可以是func()s,而不是让我的大脑弯曲这种方法。每当我必须使用这种方法时,例如使用 Echo 库,我发现我的大脑陷入了抽象的兔子洞。 #fwiw
  • 这是一个了不起的答案!非常感谢:)
【解决方案5】:

Go 中既不支持可选参数也不支持函数重载。 Go 确实支持可变数量的参数:Passing arguments to ... parameters

【讨论】:

    【解决方案6】:

    没有——也没有。根据Go for C++ programmers 文档,

    Go 不支持函数 重载且不支持用户 定义的运算符。

    我找不到同样明确的说法,即不支持可选参数,但也不支持它们。

    【讨论】:

    • "目前没有此[可选参数]的计划。" Ian Lance Taylor,Go 语言团队。 groups.google.com/group/golang-nuts/msg/030e63e7e681fd3e
    • 没有用户定义的运算符是一个糟糕的决定,因为它是任何光滑数学库背后的核心,例如线性代数的点积或叉积,通常用于 3D 图形。
    【解决方案7】:

    您可以使用映射传递任意命名参数。如果参数的类型不统一,则必须使用“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})
    }
    

    【讨论】:

    【解决方案8】:

    Go 不支持可选参数默认值函数重载,但您可以使用一些技巧来实现。 p>

    分享一个示例,您可以在一个函数中使用不同数量和类型的参数。这是一个简单的代码,便于您理解您需要添加错误处理和一些逻辑。

    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"))
    }
    

    【讨论】:

      【解决方案9】:

      您可以将它很好地封装在类似于下面的函数中。

      package main
      
      import (
              "bufio"
              "fmt"
              "os"
      )
      
      func main() {
              fmt.Println(prompt())
      }
      
      func prompt(params ...string) string {
              prompt := ": "
              if len(params) > 0 {
                      prompt = params[0]
              }
              reader := bufio.NewReader(os.Stdin)
              fmt.Print(prompt)
              text, _ := reader.ReadString('\n')
              return text
      }
      

      在本例中,提示符前默认有一个冒号和一个空格。 . .

      : 
      

      。 . .但是,您可以通过向提示函数提供参数来覆盖它。

      prompt("Input here -> ")
      

      这将导致如下提示。

      Input here ->
      

      【讨论】:

        【解决方案10】:

        Go 语言不支持方法重载,但你可以像可选参数一样使用可变参数,也可以使用 interface{} 作为参数,但这不是一个好的选择。

        【讨论】:

          【解决方案11】:

          所以我觉得我参加这个聚会已经很晚了,但我正在寻找是否有比我已经做的更好的方法来做到这一点。这有点解决了你试图做的事情,同时也给出了可选参数的概念。

          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:

          添加了一个功能示例来展示功能与示例的对比

          【讨论】:

          • 我喜欢这个而不是其他选择。这也是我在许多库中看到的一种模式,当某些东西有不同的选项并且可以重用时,您可以创建一个结构来表示这些选项并通过参数传递选项,或者您可以nil 要使用的选项默认值。这些选项也可以记录在它们自己的结构中,您可以创建预定义的选项集。我在 GitHub 客户端库和 go-cache 库等中看到了这一点。
          • @madzohan 请不要更改我的代码示例以满足您的需求...您可以要求进行更改或在下面提供您自己的示例...您的示例从根本上改变了我的功能例子。做某事的 void 函数不需要返回来满足您的需求。
          【解决方案12】:

          我最终使用了 params 结构和可变参数 args 的组合。这样,我不必更改多个服务使用的现有接口,并且我的服务能够根据需要传递额外的参数。 golang playground 示例代码:https://play.golang.org/p/G668FA97Nu

          【讨论】:

            【解决方案13】:

            我有点晚了,但如果你喜欢流畅的界面,你可以为这样的链式调用设计你的设置器:

            type myType struct {
              s string
              a, b int
            }
            
            func New(s string, err *error) *myType {
              if s == "" {
                *err = errors.New(
                  "Mandatory argument `s` must not be empty!")
              }
              return &myType{s: s}
            }
            
            func (this *myType) setA (a int, err *error) *myType {
              if *err == nil {
                if a == 42 {
                  *err = errors.New("42 is not the answer!")
                } else {
                  this.a = a
                }
              }
              return this
            }
            
            func (this *myType) setB (b int, _ *error) *myType {
              this.b = b
              return this
            }
            

            然后这样称呼它:

            func main() {
              var err error = nil
              instance :=
                New("hello", &err).
                setA(1, &err).
                setB(2, &err)
            
              if err != nil {
                fmt.Println("Failed: ", err)
              } else {
                fmt.Println(instance)
              }
            }
            

            这类似于@Ripounet 答案中提出的功能选项习语,具有相同的好处,但也有一些缺点:

            1. 如果发生错误,它不会立即中止,因此,如果您希望构造函数经常报告错误,效率会稍低。
            2. 您必须花费一行来声明一个 err 变量并将其归零。

            然而,有一个可能的小优势,这种类型的函数调用应该更容易被编译器内联,但我真的不是专家。

            【讨论】:

            • 这是一个建造者模式
            • 嗯。如果 A 产生错误,而不是 B、C、D,并且您不关心 A,会发生什么?
            • @ЯрославРахматуллин 您可以将通话分开,例如首先构建您关心的所有内容,然后检查错误,然后设置您不关心检查的内容。或者,如果您是第一个编写构造函数的人,您可以在内部忽略错误,而不会收到设置 A 的 *error。
            【解决方案14】:

            如果你不想使用指针,你可以使用指针并将它们留空:

            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)
            }
            

            【讨论】:

            • 完全同意。有时将 nil 作为参数可能比其他更改要简单得多。
            • 正在查看是否可以使用可选参数或参数默认值,因此这是可能的; func (n *Note) save(extension string = ".txt") { ... } 使“.txt”成为文件的默认但可更改的扩展名。然而现在我明白这不是 go 背后的哲学,应该只使用单独的 Save() 和 SaveWithExtension(ext string) 函数。最好不要与之抗争,从长远来看,这样做只会让一切变得更加困难。
            • 直到你开始使用 iota 和“自动递增”常量,在这种情况下,祝你好运,使用不可修改的常量(因为当然常量很神奇并且没有内存地址)
            【解决方案15】:

            另一种可能性是使用带有字段的结构来指示其是否有效。来自 sql 的 null 类型,例如 NullString 很方便。不必定义自己的类型很好,但如果您需要自定义数据类型,您始终可以遵循相同的模式。我认为从函数定义中可以清楚地看到可选性,并且几乎不需要额外的代码或工作。

            举个例子:

            func Foo(bar string, baz sql.NullString){
              if !baz.Valid {
                    baz.String = "defaultValue"
              }
              // the rest of the implementation
            }
            

            【讨论】:

              猜你喜欢
              • 2019-10-04
              • 2016-08-04
              • 2019-05-24
              • 1970-01-01
              • 1970-01-01
              • 2012-02-07
              • 2021-07-22
              • 2019-07-12
              相关资源
              最近更新 更多