轨道
/
Go
Go
/
练习
/
选举日
选举日

选举日

学习练习

简介

和许多其他语言一样,Go 也有指针。 如果你刚接触指针,可能会觉得它们有点神秘,但一旦用惯了,就会发现它们相当简单。 指针是 Go 中至关重要的一部分,所以花点时间真正理解它们吧。

在深入细节之前,值得先理解指针的用途。 指针是一种与程序其他部分共享内存的方式,它之所以有用,主要有两个原因:

  1. 当数据量很大时,在函数之间传递数据还要复制一份,效率极低。 改为传递数据所存储的内存位置,就能大幅降低程序的资源占用。
  2. 通过在函数之间传递指针,我们可以直接访问和修改那份唯一的数据副本,这意味着一个函数所做的修改,在函数结束后会立即被程序的其他部分看到。

变量与内存

假设我们有一个普通的整数变量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 年举行一次选举,选出一位新主席。

在今年的选举中,大家决定需要一套新的数字化计票系统。 学校需要你帮忙构建这套新系统。

1. 创建计票器

新的投票系统首先需要的东西之一就是计票器。 这个计票器用来记录某位特定候选人获得的票数。

创建一个函数 NewVoteCounter,它接受某位候选人的初始票数,并返回一个指向 int 的指针,该指针用给定的初始票数初始化。

var initialVotes int
initialVotes = 2

var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true

2. 从计票器获取票数

现在你有办法创建新的计票器了!但你意识到,新系统还需要一种从计票器获取票数的方法。

创建一个函数 VoteCount,它接收一个计票器(*int)作为实参,并返回该计票器中的票数。 如果计票器为 nil,就认为这个计票器没有票数:

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

VoteCount(voteCounter)
// => 3

var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0

3. 增加计票器的票数

终于到了计票的时候了!现在你需要一种增加计票器票数的方法。

创建一个函数 IncrementVoteCount,它接收一个计票器(*int)和一个票数作为实参,并把计票器的票数增加该数量。 你可以假设传入的指针永远不是 nil。

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

IncrementVoteCount(voteCounter, 2)

votes == 5          // true
*voteCounter == 5   // true

4. 创建选举结果

所有票都计完后,是时候准备向全校公布结果了。 为此,你发现只有票数计票器还不够。 需要一种把票数与特定候选人关联起来的方式。

创建一个函数 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
}

5. 公布结果

是时候向全校公布新主席了! 新主席将在学校那些小型数字留言板上公布。 消息应显示新主席的名字和获得的票数,格式如下:<candidate_name> (<votes>)。 这样一条消息的示例:"Peter (51)"。

创建一个函数 DisplayResult,它接收一个 *ElectionResult 作为实参,并返回包含待显示消息的字符串。

var result *ElectionResult
result = &ElectionResult{
    Name: "John",
    Votes: 32,
}

DisplayResult(result)
// => John (32)

6. 重新计票

为了确保最终结果准确,重新统计了票数。 在重新计票中发现,有些候选人的票数差了一票。

创建一个函数 DecrementVotesOfCandidate,它接收最终结果和某个候选人的名字,你需要把该候选人的票数减一。 最终结果以 map[string]int 的形式给出,其中键是候选人的名字,值是他们的总票数。

var finalResults = map[string]int{
    "Mary":  10,
    "John":  51,
}

DecrementVotesOfCandidate(finalResults, "Mary")

finalResults["Mary"]
// => 9
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Go Exercism

准备好开始 选举日 了吗?

注册 Exercism,借助 34 个概念165 个练习 和真人导师指导,学习并掌握 Go,全部免费。