书读百遍其义自见, 多看多问多巩固基础
下面是对https://studygolang.com/
每日一题的合集; 如有侵权,请联系我删除!
Go每日一题 (map 循环)
下面代码里的 counter 的输出值?
1 | func main() { |
- A. 2
- B. 3
- C. 2 或 3
答案解析
参考答案及解析:C。
for range map
是无序的,如果第一次循环到 A,则输出 3;否则输出 2。
Go每日一题 (线程 协程)
关于协程,下面说法正确是()
- A. 协程和线程都可以实现程序的并发执行;
- B. 线程比协程更轻量级;
- C. 协程不存在死锁问题;
- D. 通过 channel 来进行协程间的通信;
答案解析
参考答案及解析:AD。
Go每日一题 (for 循环)
关于循环语句,下面说法正确的有()
- A. 循环语句既支持 for 关键字,也支持 while 和 do-while;
- B. 关键字 for 的基本使用方法与 C/C++ 中没有任何差异;
- C. for 循环支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环;
- D. for 循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量;
答案解析
参考答案及解析:CD。
Go每日一题 (多重赋值)
下面代码输出正确的是?
1 | func main() { |
- A. s: [Z,B,C]
- B. s: [A,Z,C]
答案解析
参考答案及解析:A。
知识点:多重赋值。
多重赋值分为两个步骤,有先后顺序:
- 计算等号左边的索引表达式和取址表达式,接着计算等号右边的表达式;
- 赋值;
所以本例,会先计算 s[i-1],等号右边是两个表达式是常量,所以赋值运算等同于 i, s[0] = 2, “Z”。
Go每日一题 (强制类型转化)
关于类型转化,下面选项正确的是?
1 | A. |
答案解析
参考答案及解析:C。
知识点:强制类型转化。
Go每日一题 (switch)
关于switch语句,下面说法正确的有?
- A. 条件表达式必须为常量或者整数;
- B. 单个case中,可以出现多个结果选项;
- C. 需要用break来明确退出一个case;
- D. 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
答案解析
参考答案及解析:BD。
Go每日一题 (类型断言 方法集)
如果 Add() 函数的调用代码为:
1 | func main() { |
则Add函数定义正确的是:
- A.
1 | type Integer int |
- B.
1 | type Integer int |
- C.
1 | type Integer int |
- D.
1 | type Integer int |
答案解析
参考答案及解析:AC。
知识点:类型断言、方法集。
go 中有些的变量不可以寻址,指的是不能通过&获得其地址。
所以 func(*A) 只能接收 *A, func(A) 可以接收 A 或者 *A ,通过指针一定能得到变量的值 *A -> A
func(A) 可以接收 *A 和 A,func(*A) 只能接收 A,因为有些变量不可寻址(&获取地址)
Go每日一题 (循环 defer)
下面这段代码输出什么,说明原因。
1 | func main() { |
- 扩展题目
1 | type Test struct { |
答案解析
1 | 0 -> 3 |
解析:这是新手常会犯的错误写法,for range 循环的时候会创建每个元素的副本,而不是元素的引用,所以 m[key] = &val 取的都是变量 val 的地址,所以最后 map 中的所有元素的值都是变量 val 的地址,因为最后 val 被赋值为3,所有输出都是3.
正确的写法:
1 | func main() { |
Go每日一题 (异常 defer)
下面这段代码输出的内容
1 | package main |
答案解析
打印后
打印中
打印前
panic: 触发异常
解析:defer 的执行顺序是后进先出。当出现 panic 语句的时候,会先按照 defer 的后进先出的顺序执行,最后才会执行panic。
Go每日一题 (map)
map 的 key 为什么是无序的?
在遍历 map 的时候,我们会发现,输出的 key 是无序的。为什么?
答案解析
map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 就要远走高飞了(bucket 序号加上了 2^B)。
而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。
这样,遍历 map 的结果就不可能按原来的顺序了。
当然,如果我就一个 hard code 的 map,我也不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧。的确是这样,但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错。
当然,Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。
多说一句,“迭代 map 的结果是无序的”这个特性是从 go 1.0 开始加入的。
Go每日一题 (slice append)
下面两段代码输出什么。
1 | // 1. |
答案解析
代码 1 输出:[0 0 0 0 0 1 2 3]
代码 2 输出:[1 2 3 4]
参考解析:这道题考的是使用 append
向 slice
添加元素,第一段代码常见的错误是 [1 2 3],需要注意。
Go每日一题 (map 线程安全)
Go 的 map 可以边遍历边删除吗?
答案解析
map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic
。
上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。
一般而言,这可以通过读写锁来解决:sync.RWMutex
。
读之前调用 RLock()
函数,读完之后调用 RUnlock()
函数解锁;写之前调用 Lock()
函数,写完之后,调用 Unlock() 解锁。
另外,sync.Map
是线程安全的 map,也可以使用。
Go每日一题 (func 多返回值)
下面这段代码有什么缺陷:
1 | func sum(x, y int)(total int, error) { |
答案解析
第二个返回值没有命名。
在函数有多个返回值时,只要有一个返回值有命名,其他的也必须命名。
如果有多个返回值必须加上括号()
;如果只有一个返回值且命名也必须加上括号()
。
这里的第一个返回值有命名 total,第二个没有命名,所以错误。
Go每日一题 (map 取地址)
以下代码是否能编译通过?
1 | package main |
答案解析
这个问题,相当于问:可以对 map 的元素直接取地址吗?
以上代码编译报错:
./main.go:8:15: invalid operation: cannot take address of m[“qcrao”] (map index expression of type int)
即无法对 map
的 key
或 value
进行取址。
如果通过其他 hack 的方式,例如 unsafe.Pointer
等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。
Go每日一题 (map 相等判断)
如何确认两个 map 是否相等?
答案解析
map 深度相等的条件:
- 都为 nil
- 非空、长度相等,指向同一个 map 实体对象
- 相应的 key 指向的 value “深度”相等
直接将使用 map1 == map2
是错误的。这种写法只能比较 map 是否为 nil。
1 | package main |
输出结果:
true
true
因此只能是遍历 map 的每个元素,比较元素是否都是深度相等。
Go每日一题 (struct)
空 struct{} 占多少空间?有什么用途?
答案解析
使用空结构体 struct{}
可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
1 | fmt.Println(unsafe.Sizeof(struct{}{})) // 0 |
比如使用 map 表示集合时,只关注 key,value 可以使用 struct{}
作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
1 | type Set map[string]struct{} |
再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{}
代替。
1 | func main() { |
再比如,声明只包含方法的结构体。
1 | type Lamp struct{} |
Go每日一题 (init)
init()
函数是什么时候执行的?
答案解析
init()
函数是 Go 程序初始化的一部分。
Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init()
函数。
同一个包,甚至是同一个源文件可以有多个 init()
函数。init()
函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init()
函数的执行顺序不作保证。
一句话总结:
import
–>const
–>var
–>init()
–>main()
- 示例:
1 | package main |
Go每日一题 (new make)
new()
与 make()
的区别
答案解析
new(T)
和 make(T,args)
是 Go 语言内建函数,用来分配内存,但适用的类型不同。
new(T)
会为 T 类型的新值分配已置零的内存空间,并返回地址(指针),即类型为 *T 的值。
换句话说就是,返回一个指针,该指针指向新分配的、类型为 T 的零值。new()
适用于值类型,如数组、结构体等。
make(T,args)
返回初始化之后的 T 类型的值,这个值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。make()
只适用于 slice、map 和 channel。
Go每日一题 (new make)
下面这段代码能否通过编译,不能的话原因是什么;如果通过,输出什么。
1 | func main() { |
答案解析
不能通过编译,new([]int)
之后的 list 是一个 *[]int
类型的指针,不能对指针执行 append
操作。可以使用 make()
初始化之后再用。
同样的,map 和 channel 建议使用 make() 或字面量的方式初始化,不要用 new()
。
Go每日一题 (slice append)
下面这段代码能否通过编译,如果可以,输出什么?
1 | func main() { |
答案解析
不能通过编译。
append() 的第二个参数不能直接使用 slice,需使用 … 操作符,
将一个切片追加到另一个切片上:append(s1,s2…)。或者直接跟上元素,形如:append(s1,1,2,3)。
Go每日一题 (bit)
以下代码输出什么?
1 | package main |
- A:5
- B:+Inf
- C:panic
- D:不能编译
答案解析
正确答案:D
|
操作是按位或操作符,它的操作数只能是整数,而上面这道题的操作数是 float64,因此编译不通过。
Go每日一题 (接口 内存分配)
Go 1.15 中 var i interface{} = a 会有额外堆内存分配吗?
具体代码是:
1 | var a int = 3 |
答案解析
在 Go 中,接口被实现为一对指针
1 | type iface struct { |
其中 tab 是指向类型信息的指针;data 是指向值的指针。因此,一般来说接口意味着必须在堆中动态分配该值。
然而,Go 1.15 发行说明在 runtime 部分中提到了一个有趣的改进:
Converting a small integer value into an interface value no longer causes allocation.
意思是说,将小整数转换为接口值不再需要进行内存分配。小整数是指 0 到 255 之间的数。
Go每日一题 (常量 len 位移运算)
以下程序输出什么?
1 | package main |
- A: 0 0
- B: 0 4
- C: 4 0
- D: 4 4
答案解析
4 0(即选 C)
len 是一个内置函数。在官方标准库文档关于 len 函数有这么一句:
For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification’s “Length and capacity” section for details.
明确支持,当参数是字符串字面量和简单 array 表达式,len 函数返回值是常量,这很重要。
Go每日一题 (结构体比较)
下面代码是否可以编译通过?为什么?
1 | package main |
答案解析
编译不通过。
./main.go:39:5: invalid operation: sm1 == sm2 (struct containing map[string]string cannot be compared)
只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。
Go每日一题 (常量)
下面代码有什么问题?
1 | package main |
答案解析
./main.go:9:11: invalid operation: cannot take address of cl (untyped int constant 100)
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,
在golang中,常量是无法取出地址的,因为字面量符号并没有地址而言。
Go每日一题 (slice 初始化 append)
下面这段代码能否通过编译,不能的话原因是什么;如果通过,输出什么。
1 | func main() { |
答案解析
不能通过编译
new([]int) 之后的 list 是一个 *[]int 类型的指针,不能对指针执行 append 操作。可以使用 make() 初始化之后再用。
同样的,map 和 channel 建议使用 make() 或字面量的方式初始化,不要用 new() 。
Go每日一题 (slice append)
写出程序运行的结果:
1 | package main |
答案解析
[0 0 0 0 0 0 0 0 0 0 1 2 3]
切片追加, make 初始化均为 0
Go每日一题 (变量声明)
下面这段代码能否通过编译,如果可以,输出什么?
1 | var( |
答案解析
不能通过编译。
这道题的主要知识点是变量声明的简短模式,形如:x := 100. 但这种声明方式有限制:
- 必须使用显示初始化;
- 不能提供数据类型,编译器会自动推导;
- 只能在函数内部使用简短模式;
Go每日一题 (结构体指针使用)
通过指针变量 p 访问其成员变量 name,有哪几种方式?(多选)
- A. p.name
- B. (&p).name
- C. (*p).name
- D. p->name
答案解析
答案:AC
解析:&
取址运算符,*
指针解引用。
Go每日一题 (类型别名)
下面这段代码能否通过编译?如果通过,输出什么?
1 | package main |
答案解析
编译不通过,cannot use i (type int) as type MyInt1 in assignment
Go每日一题 (数组比较)
下面这段代码输出什么?
1 | func main() { |
- A. compilation error
- B. equal
- C. not equal
答案解析
参考答案及解析:A。
./main.go:8:10: invalid operation: a == b (mismatched types [2]int and [3]int)
Go 中的数组是值类型,可比较,另外一方面,数组的长度也是数组类型的组成部分,所以 a 和 b 是不同的类型,是不能比较的,所以编译错误。