如何在Go结构体中设置默认值

269
以下是针对以下问题的多种答案和技术:
  1. 如何为Golang结构设置默认值?
  2. 如何在Golang中初始化结构
我有一些答案,但需要进一步讨论。

1
@icza,你的回答提供了一种方法,但根据问题标题,它与之完全不同或不可搜索,因为这是一个非常具体的问题。不过我会在我的答案中添加链接。 - Prateek
这里有两个问题,请选择一个。假设您选择了第一个问题(根据问题标题),请更具体地说明您之前的研究情况以及哪些答案需要更多讨论。 - Duncan Jones
9个回答

161

一个可能的想法是编写单独的构造函数

//Something is the structure we work with
type Something struct {
     Text string 
     DefaultText string 
} 
// NewSomething create new instance of Something
func NewSomething(text string) Something {
   something := Something{}
   something.Text = text
   something.DefaultText = "default text"
   return something
}

19
是的,这也是我在回答中提到的一种方式,但我们无法强制任何人仅使用此功能。 - Prateek
1
@Prateek 要么就用这个,要么使用接口,但那会很丑陋和过于复杂。 - OneOfOne
68
@Prateek 是的,如果你将该类型本身设置为未导出,则可以强制人们使用此构造函数。您可以导出函数NewSomething甚至字段TextDefaultText,但请勿导出结构体类型something本身。 - Amit Kumar Gupta
3
如果使用第三方库(例如通过reflect.New()实例化结构体),问题会变得更加严重,因为无法期望该库知道你特别命名的工厂函数。在这种情况下,除非改变语言本身,否则仅靠接口(库可以检查接口)才能解决问题。 - edam
4
设置默认值是不错的,但有时我可能想覆盖默认值。在这种情况下,我就无法使用非默认值来初始化结构体。这对我来说有些烦人。 - Juliatzin
显示剩余6条评论

110
  1. 强制使用构造函数来获取结构体。

    根据这篇文章的说法:

    一个好的设计是使您的类型未导出,但提供一个导出的构造函数如NewMyType(),在其中您可以正确地初始化您的结构体/类型。还返回一个接口类型而不是具体类型,并且该接口应包含其他人想要对您的值进行的所有操作。 当然,您的具体类型必须实现该接口。

    这可以通过将类型本身设置为未导出来完成。 您可以导出NewSomething函数甚至Text和DefaultText字段,但只要不导出struct类型something即可。

  2. 另一种自定义方法是使用配置结构体来设置默认值 (选项5),但并不是好的方式。


7
该链接已经失效(404错误):http://joneisen.tumblr.com/post/53695478114/golang-and-default-values - Victor Zamanian
5
可以在“网页存档计划”(wayback machine)上找到它(https://web.archive.org/web/20160818125551/https://joneisen.tumblr.com/post/53695478114/golang-and-default-values)。 - n8henrie
1
就我个人而言,我认为是“选项3”-至少在wayback机器链接中是这样的。(那里没有“选项5”)。 - decimus phostle
第一条引用已被修改为:“您可以选择使您的类型未导出,并提供一个导出的构造函数,例如NewMyType(),在其中您可以正确地初始化您的结构/类型。...这样您就不必担心不正确的初始化了。” - starriet
这种方法存在一个问题。在New()函数中,您将不得不为结构体中的每个字段添加一个参数,如果您有许多字段,则对于New()的调用者来说,这变得非常麻烦。显然,您不能将Options结构作为参数传递,因为那样会导致我们试图解决的同样的问题。相反,我选择了在New()中没有参数,它只是返回具有自定义默认值的结构体。想法是调用者调用New(),然后在设置字段之后。但现在由于它是接口,他们无法这样做。 - kebab-case

60

Victor Zamanian的答案中选项1存在一个问题,如果该类型未导出,则您软件包的用户无法将其声明为函数参数等的类型。解决此问题的一种方法是导出接口而不是结构体。

package candidate
// Exporting interface instead of struct
type Candidate interface {}
// Struct is not exported
type candidate struct {
    Name string
    Votes uint32 // Defaults to 0
}
// We are forced to call the constructor to get an instance of candidate
func New(name string) Candidate {
    return candidate{name, 0}  // enforce the default value here
}

我们可以使用导出的Candidate接口声明函数参数类型。我唯一能看到这种解决方案的劣势是,所有方法都需要在接口定义中声明,但你可以认为这是一个好习惯。


如果您希望访问“c.Name”,是否需要在接口中使“候选人”重复“名称字符串”? - Eric Burel
在这里将NameVotes字段大写可能会让人感到困惑。它们可能对于json.Marshal或类似的反射操作很有用,但对于正常访问来说,小写字段一样好用。interface无法导出字段,只能导出方法(即需要方法才能使用)。 - kubanczyk

44

有一种使用标签的方法可以实现这一点,允许拥有多个默认值。

假设你有以下结构体,其中包含两个默认标签default0default1

type A struct {
   I int    `default0:"3" default1:"42"`
   S string `default0:"Some String..." default1:"Some Other String..."`
}

现在可以设置默认值。

func main() {

ptr := &A{}

Set(ptr, "default0")
fmt.Printf("ptr.I=%d ptr.S=%s\n", ptr.I, ptr.S)
// ptr.I=3 ptr.S=Some String...

Set(ptr, "default1")
fmt.Printf("ptr.I=%d ptr.S=%s\n", ptr.I, ptr.S)
// ptr.I=42 ptr.S=Some Other String...
}

这里是在Playground中的完整程序:https://play.golang.org/p/rFql2x0Klm4

如果你对更复杂的例子感兴趣,比如使用切片和映射表,可以看一下creasty/defaultse


1
非常感谢!我开始编写与库建议相同的代码,并遇到了这篇文章。它正是你所期望的(https://github.com/creasty/defaults)。如果您没有值,它将设置默认值,但是如果您为变量分配了一个值,则不会分配默认值。它与yaml.v2库非常兼容,效果很好。 - Nordes
4
这是一个不错的替代方案,但它不是来自于golang。你有一个使用反射的通用构造函数。一个真正的默认值会在每个新结构体实例上自动设置。 - ton

6

来自 https://golang.org/doc/effective_go.html#composite_literals:

有时候零值并不够好,需要一个初始化构造函数,就像这个例子从os包中派生出来的一样。

    func NewFile(fd int, name string) *File {
      if fd < 0 {
        return nil
      }
      f := new(File)
      f.fd = fd
      f.name = name
      f.dirinfo = nil
      f.nepipe = 0
      return f
}

4

做类似这样的东西怎么样:

// Card is the structure we work with
type Card struct {
    Html        js.Value
    DefaultText string `default:"html"` // this only works with strings
}

// Init is the main function that initiate the structure, and return it
func (c Card) Init() Card {
    c.Html = Document.Call("createElement", "div")
    return c
}

然后按如下方式调用:

c := new(Card).Init()

3

我发现这个帖子非常有帮助和教育意义。其他回答已经提供了很好的指导,但我想用一种易于参考(即复制粘贴)的方法来总结我的心得体会:

package main

import (
    "fmt"
)

// Define an interface that is exported by your package.
type Foo interface {
  GetValue() string // A function that'll return the value initialized with a default.
  SetValue(v string) // A function that can update the default value.
}

// Define a struct type that is not exported by your package.
type foo struct {
  value string
}

// A factory method to initialize an instance of `foo`,
// the unexported struct, with a default value.
func NewFoo() Foo {
  return &foo{
    value: "I am the DEFAULT value.",
  }
}

// Implementation of the interface's `GetValue`
// for struct `foo`.
func (f *foo) GetValue() string {
  return f.value
}

// Implementation of the interface's `SetValue`
// for struct `foo`.
func (f *foo) SetValue(v string) {
  f.value = v
}

func main() {
  f := NewFoo()
  fmt.Printf("value: `%s`\n", f.GetValue())
  f.SetValue("I am the UPDATED value.")
  fmt.Printf("value: `%s`\n", f.GetValue())
}

2
关于这个例子,有一个需要注意的地方:Go 不鼓励在 getter 的名称中加入 Get。相反,你的 Foo 接口可以声明 Value() stringSetValue(string)。请参见 https://go.dev/doc/effective_go#Getters。 - bestbeforetoday

1
一种方法是:

这样做的方式是:

// declare a type
type A struct {
    Filed1 string
    Field2 map[string]interface{}
}

所以,每当你需要一个自定义类型的新变量时,只需调用NewA函数。此外,你可以将函数参数化以可选地将值分配给结构字段。
func NewA() *A {
    return &A{
        Filed1: "",
        Field2: make(map[string]interface{}),
    }
}

如果您导出类型,那么默认构造函数可以被简单地绕过。 - blackgreen

-1

在Go结构体中设置默认值,我们使用匿名结构体:

Person := struct {
    name      string
    age       int
    city      string
}{
    name:      "Peter",
    age:        21,
    city:      "Noida",
}

fmt.Println(Person)


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