Golang易错知识点 #
GoMock #
- GoMock可以对interface打桩
- GoMock可以对类成员函数打桩
- GoMock可以对函数打桩
- GoMock打桩后的依赖注入可以通过GoStub完成
GoStub #
- GoStub可以对全局变量打桩
- GoStub可以对函数打桩
- GoStub可以动态打桩,比如对一个函数打桩后,多次调用该函数会有不同的行为
作用域 #
func main() {
a := 12
{
a := 13
_ = a // make compiler happy
}
fmt.Println(a)
}
输出 12。
在作用域内的 a 在作用域外失效,所以输出 12。
添加方法 #
可以给任意类型添加相应的方法。这一说法是否正确 false
如果直接给int添加method会报错
任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。
序列化 #
type S struct {
A int
B *int
C float64
d func() string
e chan struct{}
}
func main() {
s := S{
A: 1,
B: nil,
C: 12.15,
d: func() string {
return "NowCoder"
},
e: make(chan struct{}),
}
_, err := json.Marshal(s)
if err != nil {
log.Printf("err occurred..")
return
}
log.Printf("everything is ok.")
return
}
没有发生错误,输出 everything is ok
尽管标准库在遇到管道/函数等无法被序列化的内容时会发生错误,但因为本题中 d 和 e 均为小写未导出变量,因此不会发生序列化错误。
指针 #
通过指针变量 p 访问其成员变量 name
p.name
(*p).name
(&p).name //false
“*”是根据指针地址去找地址指向的内存中存储的具体值,“&”是根据内存中存储的具体值去反查对应的内存地址。题目中已经说明了p是指针,也就是内存地址,要使用变量(这里是调用成员属性),当然是要先根据内存地址获取存储的具体内容,选*p。
golang中没有隐藏的this指针,这句话的含义是:
- 方法施加的对象显示传递,没有被隐藏起来
- golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达
- 方法施加的对象不需要非得是指针,也不用非得this
go语言中的指针不支持运算。
map #
var m map[string]int
m["one"]=1 //false
Make只用来创建slice,map,channel。 其中map使用前必须初始化。 append可直接动态扩容slice,而map不行。
switch #
switch后面可以不跟表达式。
switch{
case 0<=Num&&Num<=3:
fmt.Printf("0-3")
case 4<=Num&&Num<=6:
fmt.Printf("4-6")
}
与其他语言不同,go语言支持不需要表达式的写法,效果等同if else
func main() {
s := "nowcoder"
a := 0
switch s {
case "nowcoder":
a++
fallthrough
case "haha":
a++
fallthrough
default:
a++
}
fmt.Println(a)
}
输出3
fallthrough会强制执行后面的case代码,不管后面的case是不是true
常量 #
对于常量定义zero(const zero = 0.0),zero是浮点型常量,这一说法是否正确。 false
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意有一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
go语言中的++、–操作符都是后置操作符,必须跟在操作数后面,并且它们没有返回值,所以它们不能用于表达式。
go语言常量要是编译时就能确定的数据
变量 #
匿名变量 #
如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单的用一个下划线“_“来跳过这个返回值,该下划线对应的变量叫匿名变量
init函数 #
- 一个包中,可以包含多个init函数
- 程序运行时,先执行导入包的init函数,再执行本包内的init函数
- main函数只能在main包中有且仅有一个,main包中可以有一个或多个init函数
- init函数和main函数都不能被显示调用
JSON转换 #
golang中大多数数据类型都可以转化为有效的JSON文本,除了channel、complex、函数等。
在golang指针中可进行隐式转换,对指针取值,对所指对象进行序列化。
defer函数 #
func main() {
ch := make(chan struct{})
defer close(ch)
go func() {
defer close(ch)
ch <- struct{}{}
}()
i := 0
for range ch {
i++
}
fmt.Printf("%d", i)
}
输出:panic
重复关闭一个管道,会导致 panic
file,err:=os.Open("test.go")
defer file.Close()
if err!=nil{
fmt.Println("open file failed",err)
return
}
...
defer 应该放在err后,如果文件为空,close会崩溃
值类型 #
数组是一个值类型,这一说法是否正确
var x string = nil //错误
Go语言中的引用类型只有五个:
切片 映射 函数 方法 通道
nil只能赋值给上面五种通道类型的变量以及指针变量。
返回值 #
在函数的多返回值中,如果有error或bool类型,则一般放在最后一个。
取反操作 #
对变量x的取反操作是~x,这一说法是否正确。 false
^x // Go语言取反方式和C语言不同,Go语言不支持~符号
import #
- import后面跟的是包的路径,而不是包名;
- 同一个目录下可以有多个.go文件,但是只能有一个包;
- 使用第三方库时,先将源码编译成.a文件放到临时目录下,然后去链接这个.a文件,而不是go install安装的那个.a文件;
- 使用标准库时,直接链接.a文件,即使修改了源码,也不会从新编译源码;
- 不管使用的是标准库还是第三方库,源码都是必须存在的,即使使用的是.a文件。
字符串 #
字符串不支持下标操作
int #
int 和 uint 的取值范围与体系架构有关,在 32 位机中等价于 int32 和 uint32,在 64 位机中等价于 int64 和 uint64。
var i int=10
var i=10
i:=10
都正确
delete函数 #
内置函数 delete 只能删除 map,参见源码:
func delete(m map[Type]Type1, key Type)
go数组是不可变类型,切片的删除没有指定的内置函数,也不能直接删除,都是通过切片的拼接进行的,s=append(s[i:],s[:i+1])
Panic #
当内置的panic()函数调用时,外围函数或方法的执行会立即终止。然后,任何延迟执行(defer)的函数或方法都会被调用,就像其外围函数正常返回一样。最后,调用返回到该外围函数的调用者,就像该外围调用函数或方法调用了panic()一样,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。当到达main()函数时不再有可以返回的调用者,因此这个过程会终止,并将包含传入原始panic()函数中的值的调用栈信息输出到os.Stderr。
关于异常的触发,下面说法正确的是:
- 空指针解析
- 下标越界
- 除数为0
- 调用panic函数
函数执行时,如果由于Panic导致了异常,程序停止执行,然后调用延迟函数defer,就像程序正常退出一样。另外recover也是要写在延迟函数中的,如果发生异常延迟函数就不执行了,那就永远无法recover了。
异常发生后,panic之前的defer函数会被执行,但是panic之后的defer函数并不会被执行。
func Defer(name string) {
defer func(par string) {
fmt.Printf("%s", par)
}(name)
defer func() {//若把这个函数注销掉,返回johnpanic: error
err := recover()
if err != nil {
fmt.Printf("%s", err)
}
}()
name = "Lee"
panic("error")
fmt.Println(1)
defer func() {
fmt.Printf("end")
}()
}
Johnerror
错误是业务过程的一部分,而异常不是。
死锁 #
func main() {
var wg sync.WaitGroup
ans := int64(0)
for i := 0; i < 3; i++ {
wg.Add(1)
go newGoRoutine(wg, &ans)
}
wg.Wait()
}
func newGoRoutine(wg sync.WaitGroup, i *int64) {
defer wg.Done()
atomic.AddInt64(i, 1)
return
}
发生死锁
sync.Waitgroup 里面有 noCopy 结构,不应该使用值拷贝,只能使用指针传递。
go结构体传参是传值,不是传引用,newGoroutine函数里的第一个参数接收的sync.waitgroup是复制值,而不是main里定义的对象,改成*sync.Waitgroup即可
main函数 #
main函数中可以使用flag包来获取和解析命令行参数
goconvey #
- goconvey是一个支持golang的单元测试框架
- goconvey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web页面
- goconvey提供了丰富的断言简化测试用例的编写
- goconvey无法与go test集成
select #
- select机制用来处理异步IO问题
- select机制最大的一条限制就是每个case语句里必须是一个IO操作
- golang在语言级别支持select关键字
go Vendor #
关于go vendor,下面说法正确的是:
- 基本思路是将引用的外部包的源码放在当前工程的vendor目录下面
- 编译go代码会优先从vendor目录先寻找依赖包
- 有了vendor目录后,打包当前的工程代码到其他机器的$GOPATH/src下面都可以通过编译
- go vendor无法精确的引用外部包进行版本控制,不能指定引用某个特定版本的外部包;只是在开发时,将其拷贝过来,但是一旦外部包升级,vendor下的代码不会跟着升级,
channel #
关于channel的特性,下面说法正确的是:
- 给一个nil channel发送数据,造成永远阻塞
- 从一个nil channel接收数据,造成永远阻塞
- 给一个已经关闭的channel发送数据,引起Panic
- 从一个已经关闭的channel接收数据,如果缓冲区为空,则返回一个零值
func main() {
ch := make(chan struct{})
go func() {
close(ch)
ch <- struct{}{}
}()
i := 0
for range ch {
i++
}
fmt.Printf("%d", i)
}
panic
向关闭的管道发送请求会导致 panic
无缓冲的channel是同步的,而有缓冲的channel是非同步的。
func main() {
ch := make(chan struct{})
defer close(ch)
go func() {
ch <- struct{}{}
}()
i := 0
for range ch {
i++
}
fmt.Printf("%d", i)
}
死锁
考察 channel 与 for-range 一起使用时容易发生死锁的情况,这里因为 ch 没有被关闭的时机,导致死锁。
for range 就是一直取,goroutine只发了一次,所以循环只转了一下就卡在接受了
切片 #
s := make([]int) //错误
在对切片初始化的时候,make中的长度参数是必须的,容量是可以不用添加的
内存泄漏 #
关于内存泄漏,下面说法正确的是:
-
golang中检测内存泄漏主要依靠的是pprof包
-
应定期使用浏览器来查看系统的实时内存信息,及时发现内存泄漏问题
-
内存泄漏不能在编译阶段发现
匿名函数 #
匿名函数可以直接赋值给一个变量或者直接执行
Cgo #
Golang可以复用C的模块,这个功能叫Cgo,CGO是C语言和Go语言之间的桥梁,原则上无法直接支持C++的类。CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。
关键字 #
go关键字:
var和const :变量和常量的声明
var varName type 或者 varName : = value package and import: 导入 func: 用于定义函数和方法 return :用于从函数返回 defer someCode :在函数退出之前执行 go : 用于并行 select 用于选择不同类型的通讯 interface 用于定义接口 struct 用于定义抽象数据类型 break、case、continue、for、fallthrough、else、if、switch、goto、default 流程控制 chan用于channel通讯 type用于声明自定义类型 map用于声明map类型数据 range用于读取slice、map、channel数据
同步锁 #
关于同步锁,下面说法正确的是:
当一个goroutine获得Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex.
RWMutex在读锁占用的情况下,会阻止写,但不阻止读。
RWMutex在写占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占
一个goroutine持有写锁 Lock(),其他goroutine不能读、不能写;
一个goroutine持有读锁RLock(),其他goroutine 可读、不能写。
每一个Lock()都应该对应一个 UnLock()
无论是RWMutex还是Mutex,与Lock()对应的都是Unlock()
cap #
cap的作用 不支持map
arry:返回数组的元素个数
slice:返回slice的最大容量
channel:返回channel的buffer容量
接口 #
关于接口,下面说法正确的是:
-
只要两个接口拥有相同的方法列表(次序不同不要紧),那么他们就是等价的,可以相互赋值。
-
如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。
-
接口查询是否成功,要在运行期才能够确定。
-
接口赋值是否可行在编译阶段就可以知道