第一题
1.下面的两个切片声明中有什么区别?哪个更可取?
A. var a []int
B. a := []int{}
参考答案及解析:A 声明的是 nil 切片;B 声明的是长度和容量都为 0 的空切片。第一种切片声明不会分配内存,优先选择。
2. A、B、C、D 哪些选项有语法错误?
type S struct { } func f(x interface{}) { } func g(x *interface{}) { } func main() { s := S{} p := &s f(s) //A g(s) //B f(p) //C g(p) //D }
参考答案及解析:BD。函数参数为 interface{} 时可以接收任何类型的参数,包括用户自定义类型等,即使是接收指针类型也用 interface{},而不是使用 *interface{}
。
永远不要使用一个指针指向一个接口类型,因为它已经是一个指针。
3.下面 A、B 两处应该填入什么代码,才能确保顺利打印出结果?
type S struct { m string } func f() *S { return __ //A } func main() { p := __ //B fmt.Println(p.m) //print "foo" }
A. &S{"foo"} B. *f() 或者 f()
f() 函数返回参数是指针类型,所以可以用 & 取结构体的指针;B 处,如果填 *f()
,则 p 是 S 类型;如果填 f(),则 p 是 *S
类型,不过都可以使用 p.m 取得结构体的成员。
第二题
1.下面的代码有几处语法问题,各是什么?
package main import ( "fmt" ) func main() { var x string = nil if x == nil { x = "default" } fmt.Println(x) }
参考答案及解析:两个地方有语法问题。golang 的字符串类型是不能赋值 nil 的,也不能跟 nil 比较。
2.return 之后的 defer 语句会执行吗,下面这段代码输出什么?
var a bool = true func main() { defer func(){ fmt.Println("1") }() if a == true { fmt.Println("2") return } defer func(){ fmt.Println("3") }() }
参考答案及解析:2 1。defer 关键字后面的函数或者方法想要执行必须先注册,return 之后的 defer 是不能注册的, 也就不能执行后面的函数或方法
第三题
1.下面这段代码输出什么?为什么?
func main() { s1 := []int{1, 2, 3} s2 := s1[1:] s2[1] = 4 fmt.Println(s1) s2 = append(s2, 5, 6, 7) fmt.Println(s1) }
参考答案及解析:
[1 2 4]
[1 2 4]
我们知道,golang 中切片底层的数据结构是数组。当使用 s1[1:] 获得切片 s2,和 s1 共享同一个底层数组,这会导致 s2[1] = 4 语句影响 s1。
而 append 操作会导致底层数组扩容,生成新的数组,因此追加数据后的 s2 不会影响 s1。
但是为什么对 s2 赋值后影响的却是 s1 的第三个元素呢?这是因为切片 s2 是从数组的第二个元素开始,s2 索引为 1 的元素对应的是 s1 索引为 2 的元素。
2.下面选项正确的是?
func main() { if a := 1; false { } else if b := 2; false { } else { println(a, b) } }
-
A. 1 2
-
B. compilation error
参考答案及解析:A。知识点:代码块和变量作用域。
第四题
1.下面这段代码输出什么?
func main() { m := map[int]string{0:"zero",1:"one"} for k,v := range m { fmt.Println(k,v) } }
参考答案及解析:
0 zero
1 one
// 或者
1 one
0 zero
map 的输出是无序的。
2.下面这段代码输出什么?
func main() { a := 1 b := 2 defer calc("1", a, calc("10", a, b)) a = 0 defer calc("2", a, calc("20", a, b)) b = 1 } func calc(index string, a, b int) int { ret := a + b fmt.Println(index, a, b, ret) return ret }
参考答案及解析:
10 1 2 3 20 0 2 2 2 0 2 2 1 1 3 4
程序执行到 main() 函数三行代码的时候,会先执行 calc() 函数的 b 参数,即:calc(“10”,a,b),输出:10 1 2 3,得到值 3,因为
defer 定义的函数是延迟函数,故 calc(“1”,1,3) 会被延迟执行;
程序执行到第五行的时候,同样先执行 calc(“20”,a,b) 输出:20 0 2 2 得到值 2,同样将 calc(“2”,0,2) 延迟执行;
程序执行到末尾的时候,按照栈先进后出的方式依次执行:calc(“2”,0,2),calc(“1”,1,3),则就依次输出:2 0 2 2,1 1 3 4。
第五题:
1.下面这段代码输出什么?为什么?
func (i int) PrintInt () { fmt.Println(i) } func main() { var i int = 1 i.PrintInt() }
-
A. 1
-
B. compilation error
参考答案及解析:B。基于类型创建的方法必须定义在同一个包内,上面的代码基于 int 类型创建了 PrintInt() 方法,由于 int 类型和方法 PrintInt() 定义在不同的包内,所以编译出错。
解决的办法可以定义一种新的类型:
type Myint int func (i Myint) PrintInt () { fmt.Println(i) } func main() { var i Myint = 1 i.PrintInt() }
2.下面这段代码输出什么?为什么?
type People interface { Speak(string) string } type Student struct{} func (stu *Student) Speak(think string) (talk string) { if think == "speak" { talk = "speak" } else { talk = "hi" } return } func main() { var peo People = Student{} think := "speak" fmt.Println(peo.Speak(think)) }
-
A. speak
-
B. compilation error
参考答案及解析:B。编译错误 Student does not implement People (Speak method has pointer receiver),值类型 Student 没有实现接口的 Speak() 方法,而是指针类型 *Student
实现该方法。
第六题
1.下面这段代码输出什么?
const ( a = iota b = iota ) const ( name = "name" c = iota d = iota ) func main() { fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) }
参考答案及解析:0 1 1 2。知识点:iota 的用法。
iota 是 golang 语言的常量计数器,只能在常量的表达式中使用。
iota 在 const 关键字出现时将被重置为0,const中每新增一行常量声明将使 iota 计数一次。
2.下面这段代码输出什么?为什么?
type People interface { Show() } type Student struct{} func (stu *Student) Show() { } func main() { var s *Student if s == nil { fmt.Println("s is nil") } else { fmt.Println("s is not nil") } var p People = s if p == nil { fmt.Println("p is nil") } else { fmt.Println("p is not nil") } }
参考答案及解析:s is nil 和 p is not nil。这道题会不会有点诧异,我们分配给变量 p 的值明明是 nil,然而 p 却不是 nil。记住一点,当且仅当动态值和动态类型都为 nil 时,接口类型值才为 nil。上面的代码,给变量 p 赋值之后,p 的动态值是 nil,但是动态类型却是 *Student
,是一个 nil 指针,所以相等条件不成立
第七题
1.下面这段代码输出什么?
type Direction int const ( North Direction = iota East South West ) func (d Direction) String() string { return [...]string{"North", "East", "South", "West"}[d] } func main() { fmt.Println(South) }
参考答案及解析:South。知识点:iota 的用法、类型的 String() 方法。
根据 iota 的用法推断出 South 的值是 2;另外,如果类型定义了 String() 方法,当使用 fmt.Printf()、fmt.Print() 和 fmt.Println() 会自动使用 String() 方法,实现字符串的打印。
2.下面代码输出什么?
type Math struct { x, y int } var m = map[string]Math{ "foo": Math{2, 3}, } func main() { m["foo"].x = 4 fmt.Println(m["foo"].x) }
-
A. 4
-
B. compilation error
参考答案及解析:B,编译报错 cannot assign to struct field m[“foo”].x in map。错误原因:对于类似 X = Y的赋值操作,必须知道 X 的地址,才能够将 Y 的值赋给 X,但 go 中的 map 的 value 本身是不可寻址的。
有两个解决办法:
1.使用临时变量
type Math struct { x, y int } var m = map[string]Math{ "foo": Math{2, 3}, } func main() { tmp := m["foo"] tmp.x = 4 m["foo"] = tmp fmt.Println(m["foo"].x) }
2.修改数据结构
type Math struct { x, y int } var m = map[string]*Math{ "foo": &Math{2, 3}, } func main() { m["foo"].x = 4 fmt.Println(m["foo"].x) fmt.Printf("%#v", m["foo"]) // %#v 格式化输出详细信息 }
第八题
1.下面的代码有什么问题?
func main() { fmt.Println([...]int{1} == [2]int{1}) fmt.Println([]int{1} == []int{1}) }
参考答案及解析:有两处错误
-
go 中不同类型是不能比较的,而数组长度是数组类型的一部分,所以 […]int{1} 和 [2]int{1} 是两种不同的类型,不能比较;
-
切片是不能比较的;
2.下面这段代码输出什么?
var p *int func foo() (*int, error) { var i int = 5 return &i, nil } func bar() { //use p fmt.Println(*p) } func main() { p, err := foo() if err != nil { fmt.Println(err) return } bar() fmt.Println(*p) }
-
A. 5 5
-
B. runtime error
参考答案及解析:B。知识点:变量作用域。问题出在操作符:=,对于使用:=定义的变量,如果新变量与同名已定义的变量不在同一个作用域中,那么 Go 会新定义这个变量。对于本例来说,main() 函数里的 p 是新定义的变量,会遮住全局变量 p,导致执行到bar()时程序,全局变量 p 依然还是 nil,程序随即 Crash。
正确的做法是将 main() 函数修改为:
func main() { var err error p, err = foo() if err != nil { fmt.Println(err) return } bar() fmt.Println(*p) }
第九题
1.下面这段代码能否正常结束?
func main() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } }
参考答案及解析:不会出现死循环,能正常结束。
循环次数在循环开始前就已经确定,循环内改变切片的长度,不影响循环次数。
2.下面这段代码输出什么?为什么?
func main() { var m = [...]int{1, 2, 3} for i, v := range m { go func() { fmt.Println(i, v) }() } time.Sleep(time.Second * 3) }
参考答案及解析:
2 3
2 3
2 3
for range 使用短变量声明(:=)的形式迭代变量,需要注意的是,变量 i、v 在每次循环体中都会被重用,而不是重新声明。
各个 goroutine 中输出的 i、v 值都是 for range 循环结束后的 i、v 最终值,而不是各个goroutine启动时的i, v值。可以理解为闭包引用,使用的是上下文环境的值。
1.使用函数传递
for i, v := range m {
go func(i,v int) {
fmt.Println(i, v)
}(i,v)
}
2.使用临时变量保留当前值
for i, v := range m {
i := i // 这里的 := 会重新声明变量,而不是重用
v := v
go func() {
fmt.Println(i, v)
}()
}
第十天
1.下面这段代码输出什么?
func f(n int) (r int) {
defer func() {
r += n
recover()
}()
var f func()
defer f()
f = func() {
r += 2
}
return n + 1
}
func main() {
fmt.Println(f(3))
}
参考答案及解析:7。根据 5 年 Gopher 都不知道的 defer 细节,你别再掉进坑里! 提到的“三步拆解法”,第一步执行r = n +1,接着执行第二个 defer,由于此时 f() 未定义,引发异常,随即执行第一个 defer,异常被 recover(),程序正常执行,最后 return。
此题引自知识星球《Go项目实战》。
2.下面这段代码输出什么?
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
参考答案及解析:
r = [1 2 3 4 5]
a = [1 12 13 4 5]
range 表达式是副本参与循环,就是说例子中参与循环的是 a 的副本,而不是真正的 a。就这个例子来说,假设 b 是 a 的副本,则 range 循环代码是这样的
for i, v := range b {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
因此无论 a 被如何修改,其副本 b 依旧保持原值,并且参与循环的是 b,因此 v 从 b 中取出的仍旧是 a 的原值,而非修改后的值。
如果想要 r 和 a 一样输出,修复办法:
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
for i, v := range &a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}
输出:
r = [1 12 13 4 5]
a = [1 12 13 4 5]
修复代码中,使用 *[5]int
作为 range 表达式,其副本依旧是一个指向原数组 a 的指针,因此后续所有循环中均是 &a 指向的原数组亲自参与的,因此 v 能从 &a 指向的原数组中取出 a 修改后的值。