go 使用中容易忽略的点
记录一下,go使用中容易出错的点
defer&panic
写出下面代码输出内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
考点:defer执行顺序
解答:
defer 是后进先出。
panic 需要等defer 结束后才会向上传递。 出现panic的时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。
1
2
3
4
打印后
打印中
打印前
panic: 触发异常
下面代码输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
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
}
考点:defer执行顺序
解答:
需要注意到defer
执行顺序和值传递 index:1
肯定是最后执行的,但是index:1
的第三个参数是一个函数,所以最先被调用calc("10",1,2)==>10,1,2,3
;执行index:2
时,与之前一样,需要先调用calc("20",0,2)==>20,0,2,2
; 执行到b=1时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2
;最后执行index:1==>calc("1",1,3)==>1,1,3,4
1
2
3
4
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
println(DeferFunc3(1))
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
解析
考点:defer和函数返回值
需要明确一点是defer
需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数 DeferFunc1
有函数返回值t
作用域为整个函数,在return
之前defer
会被执行,所以t
会被修改,返回4
; DeferFunc2
函数中t
的作用域为函数,返回1
; DeferFunc3
返回3
下面的代码执行可能有什么问题?
1
2
3
4
5
6
7
8
9
10
11
func f(filenames []string) {
for _, name := range filenames {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
...
}
}
上面的代码中循环打开文件并进行操作,在循环中设置了defer
函数来关闭文件。但是由于defer
函数需要在函数返回时才执行,所以会先打开所有文件并处理,f()
返回前才会统一的关闭文件,当文件列表过长时,可能会导致耗光文件描述符。一种解决方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func f(filenames []string) {
for _, name := range filenames {
t(name)
}
}
func t(name string ) {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
...
}
修改为上面之后,在t()执行完之后,文件就会关闭,即一个文件处理完就关闭该文件。这样避免了之前必然存在所有文件都处于打开状态的问题。
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
import (
"fmt"
"reflect"
)
func main1() {
defer func() {
if err:=recover();err!=nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic")
}()
panic("panic")
}
func main() {
defer func() {
if err:=recover();err!=nil{
fmt.Println("++++")
f:=err.(func()string)
fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic(func() string {
return "defer panic"
})
}()
panic("panic")
}
解析
考点:panic仅有最后一个可以被revover捕获
触发panic(“panic”)后顺序执行defer,但是defer中还有一个panic,所以覆盖了之前的panic(“panic”)
1
defer panic
range遍历
以下代码有什么问题,说明原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type student struct {
Name string
Age int
}
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
}
考点:foreach
解答:
这样的写法初学者经常会遇到的,很危险! 与Java的foreach一样,都是使用副本的方式。所以m[stu.Name]=&stu
实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct
的值拷贝。 就像想修改切片元素的属性:
1
2
3
for _, stu := range stus {
stu.Age = stu.Age+10
}
也是不可行的。 大家可以试试打印出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
// 错误写法
for _, stu := range stus {
m[stu.Name] = &stu
}
for k,v:=range m{
println(k,"=>",v.Name)
}
// 正确
for i:=0;i<len(stus);i++ {
m[stus[i].Name] = &stus[i]
}
for k,v:=range m{
println(k,"=>",v.Name)
}
}
goroutine
下面的代码会输出什么,并说明原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
考点:go执行的随机性和闭包
解答:
谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是A:
均为输出10
,B:
从0~9
输出(顺序不定)。
第一个go func中i
是外部for
的一个变量,地址不变化。遍历完成后,最终i=10
。 故go func执行时,i
的值始终是10
。
第二个go func中i
是函数参数,与外部for
中的i
完全是两个变量。 尾部(i
)将发生值拷贝,go func内部指向值拷贝地址。
组合继承
下面代码会输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type People struct{}
func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}
考点:go的组合继承
解答:
这是Golang的组合模式,可以实现OOP
的继承。 被组合的类型People
所包含的方法虽然升级成了外部类型Teacher
这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA()
)调用时接受者并没有发生变化。 此时People
类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher
类型的功能。
1
2
showA
showB
select
下面代码会触发异常吗?请详细说明
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
int_chan <- 1
string_chan <- "hello"
select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}
考点:select随机性
解答:
select
会随机选择一个可用通用做收发操作。 所以代码是有可能触发异常,也有可能不会。 单个chan
如果无缓冲时将会阻塞。但结合select
可以在多个chan
间等待执行。有三点原则:
- select 中只要有一个case能return,则立刻执行。
- 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
- 如果没有一个case能return则可以执行”default”块。
默认值
请写出以下输入内容
1
2
3
4
5
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
考点:make默认值和append
解答:
make初始化是由默认值的哦,此处默认值为0
1
[0 0 0 0 0 1 2 3]
大家试试改为:
1
2
3
s := make([]int, 0)
s = append(s, 1, 2, 3)
fmt.Println(s)//[1 2 3]
map 线程安全
下面的代码有什么问题?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
考点:map线程安全
解答:
可能会出现fatal error: concurrent map read and map write. 修改一下看看效果
1
2
3
4
5
6
7
8
func (ua *UserAges) Get(name string) int {
ua.Lock()
defer ua.Unlock()
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
接口实现
以下代码能编译过去吗?为什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func main() {
var peo People = Stduent{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
考点:golang的方法集
解答:
编译不通过! 做错了!?说明你对golang的方法集还有一些疑问。 一句话:golang的方法集仅仅影响接口实现和方法表达式转化,与通过实例或者指针调用方法无关。
指针变量
以下代码打印出来什么内容,说出为什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"fmt"
)
type People interface {
Show()
}
type Student struct{}
func (stu *Student) Show() {
}
func live() People {
var stu *Student
return stu
}
func main() {
if live() == nil {
fmt.Println("AAAAAAA")
} else {
fmt.Println("BBBBBBB")
}
}
考点:interface内部结构
解答:
很经典的题! 这个考点是很多人忽略的interface内部结构。 go中的接口分为两种一种是空的接口类似这样:
1
var in interface{}
另一种如题目:
1
2
3
type People interface {
Show()
}
他们的底层结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}
可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口 结果:
1
BBBBBBB
type断言
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
i := GetValue()
switch i.(type) {
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}
}
func GetValue() int {
return 1
}
解析
考点:type
编译失败,因为type只能使用在interface
函数
下面函数有什么问题?
1
2
3
func funcMui(x,y int)(sum int,error){
return x+y,nil
}
考点:函数返回值命名
在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。
nil
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func GetValue(m map[int]string, id int) (string, bool) {
if _, exist := m[id]; exist {
return "存在数据", true
}
return nil, false
}
func main() {
intmap:=map[int]string{
1:"a",
2:"bb",
3:"ccc",
}
v,err:=GetValue(intmap,3)
fmt.Println(v,err)
}
考点:函数返回值类型
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。报:cannot use nil as type string in return argument.
new ?
// TODO 为什么不能new slice??
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
func main() {
list := new([]int)
list = append(list, 1)
fmt.Println(list)
}
考点:new
1
list:=make([]int,0)
append
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2)
fmt.Println(s1)
}
考点:append
append切片时候别漏了’…’
结构体比较
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn2 := struct {
age int
name string
}{age: 11, name: "qq"}
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}
sm1 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
if sm1 == sm2 {
fmt.Println("sm1 == sm2")
}
}
考点:结构体比较
进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。
1
2
3
4
sn3:= struct {
name string
age int
}{age:11,name:"qq"}
sn3与sn1就不是相同的结构体了,不能比较。 还有一点需要注意的是结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice。 如果该结构属性都是可以比较的,那么就可以使用“==”进行比较操作。
可以使用reflect.DeepEqual进行比较
1
2
3
4
5
if reflect.DeepEqual(sn1, sm) {
fmt.Println("sn1 ==sm")
}else {
fmt.Println("sn1 !=sm")
}
所以编译不通过: invalid operation: sm1 == sm2
iota
是否可以编译通过?如果通过,输出什么?
1
2
3
4
5
6
7
8
9
10
11
const (
x = iota
y
z = "zz"
k
p = iota
)
func main() {
fmt.Println(x,y,z,k,p)
}
解析 考点:iota 结果:
1
0 1 zz zz 4
变量定义
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
package main
var(
size :=1024
max_size = size*2
)
func main() {
println(size,max_size)
}
解析 考点:变量简短模式
变量简短模式限制:
- 定义变量同时显式初始化
- 不能提供数据类型
- 只能在函数内部使用
结果:
1
syntax error: unexpected :=
const
下面函数有什么问题?
1
2
3
4
5
6
7
8
9
package main
const cl = 100
var bl = 123
func main() {
println(&bl,bl)
println(&cl,cl)
}
考点:常量 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,
1
cannot take the address of cl
goto
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
package main
func main() {
for i:=0;i<10 ;i++ {
loop:
println(i)
}
goto loop
}
考点:goto goto不能跳转到其他函数或者内层代码
1
goto loop jumps into block starting at .\main.go:5:25
alias
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main() {
type MyInt1 int
type MyInt2 = int
var i int =9
var i1 MyInt1 = i
var i2 MyInt2 = i
fmt.Println(i1,i2)
}
考点:**Go 1.9 新特性 Type Alias **
基于一个类型创建一个新类型,称之为defintion
;基于一个类型创建一个别名,称之为alias
。 MyInt1
为称之为defintion
,虽然底层类型为int
类型,但是不能直接赋值,需要强转; MyInt2
称之为alias
,可以直接赋值。
结果:
1
cannot use i (type int) as type MyInt1 in assignment
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
type User struct {
}
type MyUser1 User
type MyUser2 = User
func (i MyUser1) m1(){
fmt.Println("MyUser1.m1")
}
func (i User) m2(){
fmt.Println("User.m2")
}
func main() {
var i1 MyUser1
var i2 MyUser2
i1.m1()
i2.m2()
}
考点:**Go 1.9 新特性 Type Alias **
因为MyUser2完全等价于User,所以具有其所有的方法,并且其中一个新增了方法,另外一个也会有。 但是
1
i1.m2()
是不能执行的,因为MyUser1没有定义该方法。 结果:
1
2
MyUser1.m1
User.m2
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type T1 struct {
}
func (t T1) m1(){
fmt.Println("T1.m1")
}
type T2 = T1
type MyStruct struct {
T1
T2
}
func main() {
my:=MyStruct{}
my.m1()
}
解析 考点:**Go 1.9 新特性 Type Alias **
是不能正常编译的,异常:
1
ambiguous selector my.m1
结果不限于方法,字段也也一样;也不限于type alias
,type defintion
也是一样的,只要有重复的方法、字段,就会有这种提示,因为不知道该选择哪个。 改为:
1
2
my.T1.m1()
my.T2.m1()
type alias
的定义,本质上是一样的类型,只是起了一个别名,源类型怎么用,别名类型也怎么用,保留源类型的所有方法、字段等。
变量作用域
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"errors"
"fmt"
)
var ErrDidNotWork = errors.New("did not work")
func DoTheThing(reallyDoIt bool) (err error) {
if reallyDoIt {
result, err := tryTheThing()
if err != nil || result != "it worked" {
err = ErrDidNotWork
}
}
return err
}
func tryTheThing() (string,error) {
return "",ErrDidNotWork
}
func main() {
fmt.Println(DoTheThing(true))
fmt.Println(DoTheThing(false))
}
考点:变量作用域
因为 if 语句块内的 err 变量会遮罩函数作用域内的 err 变量,结果:
改为:
1
2
3
4
5
6
7
8
9
10
func DoTheThing(reallyDoIt bool) (err error) {
var result string
if reallyDoIt {
result, err = tryTheThing()
if err != nil || result != "it worked" {
err = ErrDidNotWork
}
}
return err
}
闭包
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
func test() []func() {
var funs []func()
for i:=0;i<2 ;i++ {
funs = append(funs, func() {
println(&i,i)
})
}
return funs
}
func main(){
funs:=test()
for _,f:=range funs{
f()
}
}
考点:闭包延迟求值
for循环复用局部变量i,每一次放入匿名函数的应用都是同一个变量。 结果:
1
2
0xc042046000 2
0xc042046000 2
如果想不一样可以改为:
1
2
3
4
5
6
7
8
9
10
func test() []func() {
var funs []func()
for i:=0;i<2 ;i++ {
x:=i
funs = append(funs, func() {
println(&x,x)
})
}
return funs
}
编译执行下面代码会出现什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
func test(x int) (func(),func()) {
return func() {
println(x)
x+=10
}, func() {
println(x)
}
}
func main() {
a,b:=test(100)
a()
b()
}
解析 考点:闭包引用相同变量* 结果:
1
2
100
110