和许多其他语言一样,Go 也有指针。 如果你刚接触指针,可能会觉得它们有点神秘,但一旦用惯了,就会发现它们相当简单。 指针是 Go 中至关重要的一部分,所以花点时间真正理解它们吧。
在深入细节之前,值得先理解指针的用途。 指针是一种与程序其他部分共享内存的方式,它之所以有用,主要有两个原因:
假设我们有一个普通的整数变量a:
var a int
当我们声明一个变量时,Go 必须找到内存中的一块地方来存储它的值。 这个过程对我们来说基本上是透明的:当我们需要取出那块内存里存的值时,只要用变量名引用它就可以了。
例如,当我们写a + 2时,实际上是在取出变量a所关联的内存中存储的值,并给它加上 2。
同样,当我们需要修改a那块内存中的值时,可以使用变量名进行赋值:
a = 3
与a关联的那块内存现在将存储值3。
变量让我们能够引用内存中的值,但有时知道变量所指向的内存地址会很有用。指针保存的就是这些值的内存地址。 声明指针类型的变量时,只需在底层类型前面加一个星号:
var p *int // 'p' contains the memory address of an integer
这里我们声明了一个变量p,它的类型是“指向 int 的指针”(*int)。
这意味着p将保存一个整数的内存地址。
指针的零值是nil,因为nil指针不保存任何内存地址。
要找出变量值的内存地址,可以使用&运算符。
例如,如果我们想找到变量a的内存地址并把它存到指针p里,可以这样做:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
有了指针之后,我们可能想知道该指针所代表的内存地址中存储的值。
可以使用*运算符做到这一点:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
*p这个操作会取出p中存储的内存地址处的值。
这个操作通常称为“解引用”。
我们也可以用解引用运算符给指针所引用的内存地址赋一个新值:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
给*pa赋值会改变pa所保存的内存地址中存储的值。
由于pa保存的是a的内存地址,给*pa赋值实际上就是在改变a的值!
不过要小心:在解引用之前,一定要检查指针是否为nil。
解引用nil指针会让程序在运行时崩溃!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
到目前为止,我们看到的都只是指向基本类型值的指针。 我们也可以为结构体创建指针:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
我们也可以创建一个新的Person,并立即存储一个指向它的指针:
var p *Person
p = &Person{Name: "Peter", Age: 22}
当我们有一个指向结构体的指针时,访问其中某个字段之前不需要先解引用这个指针:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
切片和映射是特殊的类型,因为它们的实现中已经包含指针。 这意味着大多数情况下,我们不需要为这些类型创建指针来共享它们值的内存地址。 假设我们有一个函数,用来把映射中某个键的值加一:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
如果我们创建一个映射并调用这个函数,函数对映射所做的修改会在函数结束后保留下来。 这和使用指针时得到的行为类似,但请注意,在这个例子里我们完全没有使用取地址/解引用,也没有用到任何指针语法:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
修改切片中已有的元素时也是如此。
不过,像append这样返回新切片的操作是个特例,它可能不会修改函数外部的切片。
这与切片的内部工作方式有关,本练习不会详细讲解,因为这属于更进阶的话题。
如果你真的好奇,可以在 Go 博客:append 的机制 中读到更多内容。
你附近的一所学校有一个非常活跃的学生会。 学生会由一名主席管理,每 2 年举行一次选举,选出一位新主席。
在今年的选举中,大家决定需要一套新的数字化计票系统。 学校需要你帮忙构建这套新系统。
新的投票系统首先需要的东西之一就是计票器。 这个计票器用来记录某位特定候选人获得的票数。
创建一个函数 NewVoteCounter,它接受某位候选人的初始票数,并返回一个指向 int 的指针,该指针用给定的初始票数初始化。
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
现在你有办法创建新的计票器了!但你意识到,新系统还需要一种从计票器获取票数的方法。
创建一个函数 VoteCount,它接收一个计票器(*int)作为实参,并返回该计票器中的票数。
如果计票器为 nil,就认为这个计票器没有票数:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
终于到了计票的时候了!现在你需要一种增加计票器票数的方法。
创建一个函数 IncrementVoteCount,它接收一个计票器(*int)和一个票数作为实参,并把计票器的票数增加该数量。
你可以假设传入的指针永远不是 nil。
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
所有票都计完后,是时候准备向全校公布结果了。 为此,你发现只有票数计票器还不够。 需要一种把票数与特定候选人关联起来的方式。
创建一个函数 NewElectionResult,它接收候选人的名字和票数,并返回一个新的选举结果。
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
选举结果的结构体已经为你创建好了,它定义为:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
是时候向全校公布新主席了!
新主席将在学校那些小型数字留言板上公布。
消息应显示新主席的名字和获得的票数,格式如下:<candidate_name> (<votes>)。
这样一条消息的示例:"Peter (51)"。
创建一个函数 DisplayResult,它接收一个 *ElectionResult 作为实参,并返回包含待显示消息的字符串。
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
为了确保最终结果准确,重新统计了票数。 在重新计票中发现,有些候选人的票数差了一票。
创建一个函数 DecrementVotesOfCandidate,它接收最终结果和某个候选人的名字,你需要把该候选人的票数减一。
最终结果以 map[string]int 的形式给出,其中键是候选人的名字,值是他们的总票数。
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9