英文:
Go, is this everything you need to know for assignments and initialization?
问题
Go语言在变量初始化方面有很多不同的方式,让人难以置信。也许是因为我没有完全理解,或者Go语言是一个事后加入的大型项目。很多东西感觉不太自然,看起来像是在发现缺失的功能后添加的。初始化就是其中之一。
以下是我理解的情况:
- 有值和指针两种类型。值可以使用
var =
或:=
进行初始化。 :=
只能在方法内部使用。- 使用
new
或&
来创建值和引用,它们只适用于复合类型。 - 有全新的方式来创建映射和切片。
- 使用
make
或var x []int
来创建切片和映射。注意没有使用=
或:=
。
对于新手来说,有没有简单的方法来理解所有这些?阅读规范会让人感到零散和困惑。
英文:
It is just mind boggling how many different ways Go has for variable initialization. May be either i don't understand this completely or Go is a big after thought. Lot of things don't feel natural and looks like they added features as they found them missing. Initialization is one of them.
Here is running app on Go playground showing different ways of initialization
Here is what i understand
- There are values and pointers. Values are initiated using
var =
or:=
. :=
only works inside the methods- To Create value and reference you use new or
&
. And they only work on composite types. - There are whole new ways of creating maps and slices
- Create slice and maps using either make or var x []int. Noticing there is no
=
or:=
Are there any easy way to understand all this for newbies? Reading specs gives all this in bits and pieces everywhere.
答案1
得分: 12
首先,提到一些不正确的陈述:
> 创建值和引用时,使用new或&。它们只适用于复合类型。
new()适用于所有类型,&不适用。
> 使用make或var x []int来创建切片和映射。注意没有=或:=。
实际上,你可以使用:=
来创建x := []int{}
,甚至是x := []int(nil)
。然而,第一种方式只用于创建长度为0且不为nil的切片,第二种方式从未使用过,因为var x []int
更好。
让我们从头开始。你可以使用var name T
来初始化变量。类型推断是为了让你少输入一些内容,并避免担心函数返回的类型名称。所以你可以这样做var name = f()
。
如果你想分配一个指针,你需要先创建被指向的变量,然后获取它的指针:
var x int
var px = &x
需要两个语句可能会有些麻烦,所以他们引入了一个名为new()的内置函数。这样你就可以用一条语句完成:var px = new(int)
然而,使用new()创建指针仍然存在一个问题。如果你正在构建一个包含期望指针的结构体的大型字面量,这是一种非常常见的情况。所以他们添加了一种(仅适用于复合字面量)处理这个问题的方式。
var x = []*T{
&T{x: 1},
&T{x: 2},
}
在这种情况下,我们希望为指针引用的结构体的字段分配不同的值。使用new()处理这个问题将会很糟糕,所以允许使用这种方式。
无论何时初始化一个变量,它总是被初始化为其零值。没有构造函数。这对于一些内置类型来说是个问题。具体来说,切片、映射和通道是复杂的结构。它们需要参数和初始化,而不仅仅是将内存设置为零。需要这样做的Go用户只需编写初始化函数。所以,他们写了make()
。new([]x)
返回一个指向x切片的指针,而make([]x, 5)
返回一个由长度为5的数组支持的x切片。new返回一个指针,而make返回一个值。这是一个重要的区别,所以它们是分开的。
那么:=
呢?这是一个大混乱。他们这样做是为了节省输入,但它导致了一些奇怪的行为,并且随后添加了不同的规则,直到它变得有些可用。
起初,它是var x = y
的简写形式。然而,它经常与多个返回值一起使用。而大多数这些多个返回值都是错误。所以我们经常会遇到这样的情况:
x, err := f()
但是多个错误会出现在标准函数中,所以人们开始给它们命名,比如:
x, err1 := f()
y, err2 := g()
这太荒谬了,所以他们制定了规则,即:=
只有在该作用域中尚未声明时才重新声明。但这违背了:=
的初衷,所以他们还添加了一个规则,即至少要有一个新声明。
有趣的是,这解决了95%的问题,使其可用。尽管我遇到的最大问题是,左侧的所有变量都必须是标识符。换句话说,以下代码是无效的,因为x.y
不是标识符:
x.y, z := f()
这是它与var的关系的剩余部分,var不能声明除标识符以外的任何内容。
至于为什么:=在函数作用域之外不起作用?这是为了让编写编译器更容易。在函数外部,每个部分都以关键字(var、func、package、import)开头。:=
意味着该声明将是唯一一个不以关键字开头的声明。
所以,这就是我对这个主题的小抱怨。底线是不同形式的声明在不同的领域中是有用的。
英文:
First, to mention some incorrect statements:
> To Create value and reference you use new or &. And they only work on composite types.
new() works on everything. & doesn't
> Create slice and maps using either make or var x []int. Noticing there is no = or :=
You can actually use :=
with x := []int{}
or even x := []int(nil)
. However, the first is only used when you want to make a slice of len 0 that is not nil and the second is never used because var x []int
is nicer.
Lets start from the beginning. You initialize variables with var name T
. Type inference was added because you can type less and avoid worrying about the name of a type a function returns. So you are able to do var name = f()
.
If you want to allocate a pointer, you would need to first create the variable being pointed to and then get its pointer:
var x int
var px = &x
Requiring two statements can be bothersome so they introduced a built-in function called new(). This allows you to do it in one statement: var px = new(int)
However, this method of making new pointers still has an issue. What if you are building a massive literal that has structs that expect pointers? This is a very common thing to do. So they added a way (only for composite literals) to handle this.
var x = []*T{
&T{x: 1},
&T{x: 2},
}
In this case, we want to assign different values to fields of the struct the pointers reference. Handling this with new() would be awful so it is allowed.
Whenever a variable is initialized, it is always initialized to its zero value. There are no constructors. This is a problem for some of the builtin types. Specifically slices, maps, and channels. These are complicated structures. They require parameters and initialization beyond memory being set to zero. Users of Go who need to do this simply write initialization functions. So, that is what they did, they wrote make()
. neW([]x)
returns a pointer to a slice of x while make([]x, 5)
returns a slice of x backed by an array of length 5. New returns a pointer while make returns a value. This is an important distinction which is why it is separate.
What about :=
? That is a big clusterfuck. They did that to save on typing, but it led to some odd behaviors and the tacked on different rules until it became somewhat usable.
At first, it was a short form of var x = y. However, it was used very very often with multiple returns. And most of those multiple returns were errors. So we very often had:
x, err := f()
But multiple errors show up in your standard function so people started naming things like:
x, err1 := f()
y, err2 := g()
This was ridiculous so they made the rule that :=
only re-declares if it is not already declared in that scope. But that defeats the point of :=
so they also tacked on the rule that at least one must be newly declared.
Interestingly enough, this solves 95% of its problems and made it usable. Although the biggest problem I run into with it is that all the variables on the lefthand side must be identifiers. In other words, the following would be invalid because x.y
is not an identifier:
x.y, z := f()
This is a leftover of its relation to var which can't declare anything other than an identifier.
As for why := doesn't work outside a function's scope? That was done to make writing the compiler easier. Outside a function, every part starts with a keyword (var, func, package, import). :=
would mean that declaration would be the only one that doesn't start with a keyword.
So, that is my little rant on the subject. The bottom line is that different forms of declaration are useful in different areas.
答案2
得分: 4
是的,这也是我早期感到困惑的事情之一。我制定了一些自己的经验法则,可能不是最佳实践,但它们对我很有帮助。在 Stephen 的评论中进行了调整,关于零值
-
尽可能使用
:=
,让 Go 推断类型 -
如果你只需要一个空的切片或映射(不需要设置初始容量),可以使用
{}
语法s := []string{}
m := map[string]string{}
-
使用
var
的唯一原因是将某个值初始化为零值(由 Stephen 在评论中指出)(是的,在函数外部你也需要var
或const
):var ptr *MyStruct // 这将一个指针初始化为 nil
var t time.Time // 这将一个 time.Time 初始化为零值
英文:
Yeah, this was one of those things that I found confusing early as well. I've come up with my own rules of thumb which may or may not be best practices, but they've served me well. Tweaked after comment from Stephen about zero values
-
Use
:=
as much as possible, let Go infer types -
If you just need an empty slice or map (and don't need to set an initial capacity), use
{}
syntaxs := []string{}
m := map[string]string{}
-
The only reason to use
var
is to initialize something to a zero value (pointed out by Stephen in comments) (and yeah, outside of functions you'll needvar
orconst
as well):var ptr *MyStruct // this initializes a nil pointer
var t time.Time // this initializes a zero-valued time.Time
答案3
得分: 3
我认为你的困惑来自于混淆了类型系统和声明与初始化。
在Go中,变量可以通过两种方式进行声明:使用var
关键字或使用:=
运算符。var
只是声明变量,而:=
还会给它赋予一个初始值。例如:
var i int
i = 1
等同于
i := 1
之所以可以这样做,是因为:=
假设变量i
的类型与它被初始化的表达式的类型相同。因此,由于表达式1
的类型是int
,Go知道i
被声明为一个整数。
注意,你也可以使用var
关键字显式声明和初始化变量:
var i int = 1
这是Go中唯一的两种声明/初始化构造方式。你是对的,在全局作用域中不允许使用:=
。除此之外,这两种方式是可以互换的,只要Go能够猜出你在:=
右侧使用的类型(大部分情况下都可以)。
这些构造方式适用于任何类型。类型系统与声明/初始化语法完全独立。我的意思是,关于哪些类型可以与声明/初始化语法一起使用,没有特殊规则 - 只要是类型,你都可以使用它。
有了这个理解,让我们来看看你的例子并解释一切。
示例1
// 值类型赋值 [字符串,布尔值,数字]
// = & := 赋值
// 指针类型不适用?
var value = "Str1"
value2 := "Str2"
这对指针类型也适用。关键是你必须将它们设置为类型为指针的表达式。以下是一些具有指针类型的表达式:
- 任何对
new()
的调用(例如,new(int)
的类型是*int
) - 引用现有值(如果
i
的类型是int
,那么&i
的类型是*int
)
因此,要使你的示例适用于指针:
tmp := "Str1"
var value = &tmp // value的类型为*int
value2 := new(string)
*value2 = "Str2"
示例2
// 结构体赋值
var ref1 = refType{"AGoodName"}
ref2 := refType{"AGoodName2"}
ref3 := &refType{"AGoodName2"}
ref4 := new(refType)
你在这里使用的语法refType{"AGoodName"}
用于在单个表达式中创建和初始化结构体。这个表达式的类型是refType
。
注意,这里有一个奇怪的地方。通常情况下,你不能取字面值的地址(例如,&3
在Go中是非法的)。然而,你可以取字面值结构体的地址,就像你在上面的ref3 := &refType{"AGoodName2"}
中所做的那样。这似乎非常令人困惑(对我来说确实如此)。之所以允许这样做,是因为它实际上只是一种简写形式,相当于调用ref3 := new(refType)
,然后通过*ref3 = refType{"AGoodName2"}
进行初始化。
示例3
// 数组,现在有了全新的赋值方式,= 或 := 不再适用
var array1 [5]int
这个语法实际上等同于var i int
,只是类型不再是int
,而是[5]int
(一个包含五个整数的数组)。如果我们愿意,现在可以单独设置数组中的每个值:
var array1 [5]int
array1[0] = 0
array1[1] = 1
array1[2] = 2
array1[3] = 3
array1[4] = 4
然而,这样做非常繁琐。就像整数和字符串等类型一样,数组可以有字面值。例如,1
是一个具有类型int
的字面值。在Go中,创建字面数组值的方式是显式命名类型,然后用方括号(类似于结构体字面值)给出值,如下所示:
[5]int{0, 1, 2, 3, 4}
这是一个字面值,所以我们可以像使用其他初始化器一样使用它:
var array1 [5]int = [5]int{0, 1, 2, 3, 4}
var array2 = [5]int{0, 1, 2, 3, 4}
array3 := [5]int{0, 1, 2, 3, 4}
示例4
// 切片,更多的赋值方式
var slice1 []int
切片在初始化方式上与数组非常相似。唯一的区别是它们的长度不固定,所以在命名类型时不需要给出长度参数。因此,[]int{1, 2, 3}
是一个整数切片,其初始长度为3(尽管后面可以更改)。所以,就像上面一样,我们可以这样做:
var slice1 []int = []int{1, 2, 3}
var slice2 = []int{1, 2, 3}
slice3 := []int{1, 2, 3}
示例5
var slice2 = new([]int)
slice3 := new([]int)
当类型变得复杂时,推断类型可能会变得棘手。如上所述,new(T)
返回一个指针,其类型为*T
。当类型是int(即new(int)
的类型为*int
)时,这很简单明了。然而,当类型本身也是复杂的,比如切片类型时,就会变得令人困惑。在你的示例中,slice2
和slice3
的类型都是*[]int
。例如:
slice3 := new([]int)
*slice3 = []int{1, 2, 3}
fmt.Println((*slice3)[0]) // 输出1
你可能将new
和make
混淆了。make
用于需要某种初始化的类型。对于切片,make
创建一个给定大小的切片。例如,make([]int, 5)
创建一个长度为5的整数切片。make(T)
的类型是T
,而new(T)
的类型是*T
。这确实可能会令人困惑。以下是一些示例,以帮助搞清楚:
a := make([]int, 5) // 现在a是一个包含5个整数的切片
b := new([]int) // b指向一个切片,但尚未初始化
*b = make([]int, 3) // 现在b指向一个包含5个整数的切片
示例6
// 映射
var map1 map[int]string
var map2 = new(map[int]string)
映射与切片非常相似,需要一些初始化才能正常工作。这就是为什么上面的map1
和map2
都还不能使用。你需要先使用make
:
var map1 map[int]string // 还不能使用
map1 = make(map[int]string) // 现在可以使用了
var map2 = new(map[int]string) // 类型为*map[int]string,还不能使用
*map2 = make(map[int]string) // 现在*map2可以使用了
额外说明
上面没有一个真正合适的地方来放置这个说明,所以我就放在这里吧。
在Go中,如果你声明一个变量而不初始化它,它实际上并不是未初始化的。相反,它有一个“零值”。这与C不同,C中未初始化的变量可能包含垃圾数据。
每种类型都有一个零值。大多数类型的零值都是合理的。例如,基本类型:
int
的零值是0
bool
的零值是false
string
的零值是""
(其他数字类型如int8
、uint16
、float32
等的零值都是0
或0.0
)
对于结构体和数组等复合类型,零值是递归的。也就是说,数组的零值是所有条目都设置为各自的零值的数组(例如,[3]int
的零值是[3]int{0, 0, 0}
,因为0
是int
的零值)。
还要注意的一点是,在使用:=
语法时,某些表达式的类型无法推断。主要要注意的是nil
。因此,i := nil
会产生编译错误。之所以如此,是因为nil
用于所有指针类型(以及其他一些类型),所以编译器无法知道你是指一个空的int指针,还是一个空的bool指针等等。
英文:
I think your confusion is coming from mixing up the type system with declaration and initialization.
In Go, variables can be declared in two ways: using var
, or using :=
. var
only declares the variable, while :=
also assigns an initial value to it. For example:
var i int
i = 1
is equivalent to
i := 1
The reason this is possible is that :=
simply assumes that the type of i
is the same as the type of the expression it's being initialized to. So, since the expression 1
has type int
, Go knows that i
is being declared as an integer.
Note that you can also explicitly declare and initialize a variable with the var
keyword as well:
var i int = 1
Those are the only two delcaration/initialization constructs in Go. You're right that using :=
in the global scope is not allowed. Other than that, the two are interchangable, as long as Go is able to guess what type you're using on the right-hand-side of the :=
(which is most of the time).
These constructs work with any types. The type system is completely independent from the declaration/initialization syntax. What I mean by that is that there are no special rules about which types you can use with the declaration/initialization syntax - if it's a type, you can use it.
With that in mind, let's walk through your example and explain everything.
Example 1
// Value Type assignments [string, bool, numbers]
// = & := Assignment
// Won't work on pointers?
var value = "Str1"
value2 := "Str2"
This will work with pointers. The key is that you have to be setting these equal to expressions whose types are pointers. Here are some expressions with pointer types:
- Any call to
new()
(for example,new(int)
has type*int
) - Referencing an existing value (if
i
has typeint
, then&i
has type*int
)
So, to make your example work with pointers:
tmp := "Str1"
var value = &tmp // value has type *int
value2 := new(string)
*value2 = "Str2"
Example 2
// struct assignments
var ref1 = refType{"AGoodName"}
ref2 := refType{"AGoodName2"}
ref3 := &refType{"AGoodName2"}
ref4 := new(refType)
The syntax that you use here, refType{"AGoodName"}
, is used to create and initialize a struct in a single expression. The type of this expression is refType
.
Note that there is one funky thing here. Normally, you can't take the address of a literal value (for example, &3
is illegal in Go). However, you can take the address of a literal struct value, like you do above with ref3 := &refType{"AGoodName2"}
. This seems pretty confusing (and certainly confused me at first). The reason it's allowed is that it's actually just a short-hand syntax for calling ref3 := new(refType)
and then initializing it by doing *ref3 = refType{"AGoodName2"}
.
Example 3
// arrays, totally new way of assignment now, = or := now won't work now
var array1 [5]int
This syntax is actually equivalent to var i int
, except that instead of the type being int
, the type is [5]int
(an array of five ints). If we wanted to, we could now set each of the values in the array separately:
var array1 [5]int
array1[0] = 0
array1[1] = 1
array1[2] = 2
array1[3] = 3
array1[4] = 4
However, this is pretty tedious. Just like with integers and strings and so on, arrays can have literal values. For example, 1
is a literal value with the type int
. The way you create a literal array value in Go is by naming the type explicitly, and then giving the values in brackets (similar to a struct literal) like this:
[5]int{0, 1, 2, 3, 4}
This is a literal value just like any other, so we can use it as an initializer just the same:
var array1 [5]int = [5]int{0, 1, 2, 3, 4}
var array2 = [5]int{0, 1, 2, 3, 4}
array3 := [5]int{0, 1, 2, 3, 4}
Example 4
// slices, some more ways
var slice1 []int
Slices are very similar to arrays in how they are initialized. The only difference is that they aren't fixed at a particular length, so you don't have to give a length parameter when you name the type. Thus, []int{1, 2, 3}
is a slice of integers whose length is initially 3 (although could be changed later). So, just like above, we can do:
var slice1 []int = []int{1, 2, 3}
var slice2 = []int{1, 2, 3}
slice3 := []int{1, 2, 3}
Example 5
var slice2 = new([]int)
slice3 := new([]int)
Reasoning about types can get tricky when the types get complex. As I mentioned above, new(T)
returns a pointer, which has type *T
. This is pretty straightforward when the type is an int (ie, new(int)
has type *int
). However, it can get confusing when the type itself is also complex, like a slice type. In your example, slice2
and slice3
both have type *[]int
. For example:
slice3 := new([]int)
*slice3 = []int{1, 2, 3}
fmt.Println((*slice3)[0]) // Prints 1
You may be confusing new
with make
. make
is for types that need some sort of initialization. For slices, make
creates a slice of the given size. For example, make([]int, 5)
creates a slice of integers of length 5. make(T)
has type T
, while new(T)
has type *T
. This can certainly get confusing. Here are some examples to help sort it out:
a := make([]int, 5) // s is now a slice of 5 integers
b := new([]int) // b points to a slice, but it's not initialized yet
*b = make([]int, 3) // now b points to a slice of 5 integers
Example 6
// maps
var map1 map[int]string
var map2 = new(map[int]string)
Maps, just like slices, need some initialization to work properly. That's why neither map1
nor map2
in the above example are quite ready to be used. You need to use make
first:
var map1 map[int]string // Not ready to be used
map1 = make(map[int]string) // Now it can be used
var map2 = new(map[int]string) // Has type *map[int]string; not ready to be used
*map2 = make(map[int]string) // Now *map2 can be used
Extra Notes
There wasn't a really good place to put this above, so I'll just stick it here.
One thing to note in Go is that if you declare a variable without initializing it, it isn't actually uninitialized. Instead, it has a "zero value." This is distinct from C, where uninitialized variables can contain junk data.
Each type has a zero value. The zero values of most types are pretty reasonable. For example, the basic types:
int
has zero value0
bool
has zero valuefalse
string
has zero value""
(other numeric values like int8
, uint16
, float32
, etc, all have zero values of 0
or 0.0
)
For composite types like structs and arrays, the zero values are recursive. That is, the zero value of an array is an array with all of its entries set to their respective zero values (ie, the zero value of [3]int
is [3]int{0, 0, 0}
since 0
is the zero value of int
).
Another thing to watch out for is that when using the :=
syntax, certain expressions' types cannot be inferred. The main one to watch out for is nil
. So, i := nil
will produce a compiler error. The reason for this is that nil
is used for all of the pointer types (and a few other types as well), so there's no way for the compiler to know if you mean a nil int pointer, or a nil bool pointer, etc.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论