Golang常见陷阱和误用
for...range(值复制)
go很喜欢值传递。for range结构中也是如此。简单地说,不再是原来的ints数组了,虽然它们的值是相等的,但在for range结构中无法利用返回的index和value值修改原来的数组
意思是,在for range中操作的index和value其实只是一份原来数组的复制品!看个例子:
ints := []int{1,2,3}
for index, value := range ints {
fmt.Printf("ints[%d]的地址:%p\n", index, &ints[index])
fmt.Printf("第%d个v的地址:%p\n", index, &value)
}
/*
执行结果:
ints[0]的地址:0xc000014180
第0个v的地址:0xc00000a0b8
ints[1]的地址:0xc000014188
第1个v的地址:0xc00000a0b8
ints[2]的地址:0xc000014190
第2个v的地址:0xc00000a0b8
*/
可以看出,for range中value的地址和外部ints数组的地址并不相同,而且,value的地址没有发生变化。
刚才的for range和以下的写法的效果是同等的:
ints := []int{1,2,3}
arrlen := len(ints)
var value interface{}
for index:=0; index<arrlen; index++ {
value := ints[i]
fmt.Printf("ints[%d]的地址:%p\n", index, &ints[index])
fmt.Printf("第%d个v的地址:%p\n", index, &value)
}
/*
执行结果:
ints[0]的地址:0xc000014180
第0个value的地址:0xc000042240
ints[1]的地址:0xc000014188
第1个value的地址:0xc000042240
ints[2]的地址:0xc000014190
第2个value的地址:0xc000042240
*/
到此,我们来总结一下,for range是go的语法糖,使用值拷贝的形式获取到了被遍历数组等结构的索引和值。需要注意的是每次遍历时value地址是不变的
因此,试试下面程序输出什么
slice := []int{0,1,2,3}
m := make(map[int]*int)
for key, val := range slice {
m[key] = &val
}
for k, v := range m {
fmt.Println(k, "->", *v)
}
答案是:
0 -> 3
1 -> 3
2 -> 3
3 -> 3
原因是第一次range的时候, val地址是不变的,因此map中的value都指向同一个地址
正确的写法:
slice := []int{0,1,2,3}
m := make(map[int]*int)
for key,val := range slice {
// 每次用新的变量接收val,让地址不一样
value := val
m[key] = &value
}
for k,v := range m {
fmt.Println(k,"===>",*v)
}
range数组时, 同样会复制一份数组, 参数循环的是副本而不是原数组, 因此在range中修改原数组时, 副本数组并不会被修改
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
for i, v := range a { // 此时参与循环的是一个a的副本
if i == 0 { // 在这里修改了a,但是不影响副本, 也就是不影响循环的值
a[1] = 12
a[2] = 13
}
r[i] = v // 这里i和v是副本的值, 没有被修改
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
//输出,r还是原数组,a被修改
r = [1 2 3 4 5]
a = [1 12 13 4 5]
上面的a数组如果改为切片, 那么r和a都会被修改, 因为切片复制时复制的是应用, 指向的地址是一样的
func main() {
var a = []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和a都被修改
r = [1 12 13 4 5]
a = [1 12 13 4 5]
make slice append
//1.
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
//2.
func main(){
s := make([]int, 0)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
两段代码分别输出:
[0 0 0 0 0 1 2 3]
[1 2 3]
原因是make初始化时, 若传入的第二个参数len>0, 则会填充每个元素为0值, 后面append时,会追加元素,而不会修改前面的值
new()和make()
new(T) 为一个 T 类型新值分配空间并将此空间初始化为 T 的零值,返回的是新值的地址,也就是 T 类型的指针 *T,该指针指向 T 的新分配的零值.
make(T, args) 返回的是初始化之后的 T 类型的值,这个新值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用, make 只能用于 slice,map,channel 三种类型, 并且只能是这三种对象
slice, map 和 channel 建议使用 make() 或字面量的方式初始化,不要用 new() 。
func main() {
p := new([]int) //p == nil; with len and cap 0
fmt.Println(p)
v := make([]int, 10, 50) // v is initialed with len 10, cap 50
fmt.Println(v)
/*********Output****************
&[]
[0 0 0 0 0 0 0 0 0 0]
*********************************/
(*p)[0] = 18 // panic: runtime error: index out of range
// because p is a nil pointer, with len and cap 0
p = append(p, 1) // build err
v[1] = 18 // ok
}
两个结构体比较
下面这段代码能否通过编译?不能的话,原因是什么?如果通过,输出什么?
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")
}
}
参考答案及解析:编译不通过 invalid operation: sm1 == sm2
这道题目考的是结构体的比较,有几个需要注意的地方:
- 结构体只能比较是否相等,但是不能比较大小。
- 相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关,sn3 与 sn1 就是不同的结构体;
sn3:= struct {
name string
age int
}{age:11,name:"qq"}
- 如果 struct 的所有成员都可以比较,则该 struct 就可以通过 == 或 != 进行比较是否相等,比较时逐个项进行比较,如果每一项都相等,则两个结构体才相等,否则不相等;
那什么是可比较的呢,常见的有 bool、数值型、字符、指针、数组等,像切片、map、函数等是不能比较的。
type关键字定义类型时有无"="的区别
func main(){
type myint1 int // 创建一个新的类型, 与int类型是不同的类型
type myint2 = int // 给int类型创建一个别名,不是新类型,实际还是int类型
var a myint1
a = 16
var b myint2
b = 17
fmt.Printf("a的类型:%T,b的类型:%T\n", a, b) // a的类型:main.myint1,b的类型:int
}
使用了=号, 起别名后, 不能在该别名上定义方法了
import "bufio"
type mathsqprt = bufio.Reader
func (T mathsqprt) add () { // cannot define new methods on non-local type bufio.Reader
}
想定义自己的方法,那就不能使用=号, 如下
import "bufio"
type mathsqprt bufio.Reader
func (T mathsqprt) add () {
}
nil赋值
下面赋值正确的是()
- A. var x = nil
- B. var x interface{} = nil
- C. var x string = nil
- D. var x error = nil
参考答案及解析:BD。知识点:nil 值。nil 只能赋值给指针、chan、func、interface、map 或 slice 类型的变量。强调下 D 选项的 error 类型,它是一种内置接口类型, 所以D是对的
只有接口类型才能使用类型选择 .(type)
func GetValue() int {
return 1
}
func main() {
i := GetValue()
switch i.(type) {
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}
}
编译失败。只有接口类型才可以使用类型选择
获取map中不存在的key时,会返回元素类型的零值
type person struct {
name string
}
func main() {
var m map[person]int
p := person{"mike"}
fmt.Println(m[p]) // 0
}
不定长参数...是可变的
func hello(num ...int) {
num[0] = 18
}
func main() {
a := []int{1, 2, 3}
hello(a...)
fmt.Print(a[0]) // 18
}
调用hello函数时,会将num当做切片处理,因此是可变的
截取操作符的三个参数
a := [5]int{1,2,3,4,5}
b := a[3:]
fmt.Println(s, len(s), cap(s)) // [4 5] 2 2
s := a[3:4]
fmt.Println(s, len(s), cap(s)) // [4] 1 2
t := a[3:4:4]
fmt.Println(t, len(t), cap(t)) // [4] 1 1
b1 := b[3:]
fmt.Println(b1, len(b1), cap(b1)) // panic: runtime error: slice bounds out of range [3:2]
截取操作可以带1个或 2 个或3 个参数, 假设底层数组长度为l
1个参数的形如[i:]
, 如果j
省略, 默认为原切片或者数组的长度, 由于b1是在b切片的基础上定义的, 因此b1定义时省略的j
为b
的长度2, 而起始值为3, 大于2, 因此panic
2个参数的形如[i:j]
, 那么新切片的长度为j-i
, 容量为l-i
3个参数的形如 [i:j:k]
,第三个参数k用来限制新切片的容量, 但不能超过原数组(切片)的底层数组大小l
, 新切片的长度为j-i
, 容量为k-i
即区别就是2个参数时容量为l-i
, 3个参数时容量为k-i
删除map不存在的key时不会报错
func main() {
s := make(map[string]int)
delete(s, "h") // 不会报错
fmt.Println(s["h"]) // 打印不存在的key对应的value, 返回零值, 这里为0
}
defer语句时会保存一份副本
func hello(num int) {
fmt.Println("hello:", num)
}
func main() {
i := 5
defer hello(i)
i += 10
fmt.Println("main:", i)
}
// main: 15
// hello: 5
在执行到defer语句的定义时候会保存一份副本,即把i=5时存起来, 在实际调用 hello() 函数时用,所以hello打印5.
若想在defer中打印最新的i, 则可在defer中传入引用
func hello(num *int) {
fmt.Println("hello:", *num)
}
func main() {
i := 5
defer hello(&i)
i += 10
fmt.Println("main:", i)
}
// main: 15
// hello: 15
或者将defer函数在调用hello时外面再包一层func, 这样在执行到defer定义时,只会定义defer后面的匿名函数, 不会执行函数体, 因此也不会复制当前的i, 在最终执行defer时, 才会去读取最新的i
func hello(num int) {
fmt.Println("hello:", num)
}
func main() {
i := 5
defer func(){ hello(i) }()
i += 10
fmt.Println("main:", i)
}
// main: 15
// hello: 15
defer和return
返回参数为具名参数时,defer可修改最终的返回值
func hello() (res int) {
defer func() { res++ }()
return res
}
func main() {
fmt.Println(hello()) // 1
}
返回参数为匿名参数时,defer不可修改最终的返回值
func hello() int {
var res int
defer func() { res++ }()
return res
}
func main() {
fmt.Println(hello()) // 0
}
返回参数为具名参数, 但return的是另外一个变量时
func hello() (res int) {
i := 1
defer func() { i++ }()
return i
}
func main() {
fmt.Println(hello()) // 1
}
return会分2步, 第一步将i赋值给res(拷贝复制), 第二步执行defer, 但defer中改变的还是原来的i,不会改变res变量
struct继承重写
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()
t.ShowB()
t.ShowA()的打印结果为:
showA
showB
打印出"showB"而不是"teacher showB"的原因是Teacher没有自己的ShowA方法, 因此使用的是People的ShowA方法, 在ShowA中调用的是people的ShowB, 而不是teacher的ShowB, 因此打印"showB"
t.ShowB()的打印结果为:
teacher showB
打印出"teacher showB"的原因是使用的是Teacher的ShowA方法
指针接受者与值接收者对接口实现的区别
定义一个接口A, 需实现Add方法
type A interface {
Add()
}
定义2个结构体,分别通过值接受者(B)和指针接受者(C)实现接口A
type B struct{}
func (b B)Add(){}
type C struct{}
func (c *C)Add(){}
创建变量, 其类型都为A接口类型
var a1 A = B{}
var a2 A = &B{}
var a3 A = C{}
var a4 A = &C{}
其中第三行a3的定义是会编译报错的, 其他三种定时方式不会报错:
Cannot use 'C{}' (type C) as the type A Type does not implement 'A' as the 'Add' method has a pointer receiver
对于值接受者B来说, B{}
和&B{}
都实现了A接口, 因此编译成功
对于指针接受者C来说, 只有C的指针变量才实现了A接口, 而C{}
并没有实现A接口, 所以会报错, 而&c{}
则不会报错
nil类型的操作
func main() {
// nil slice
var a []int // 也可写作: a := ([]int)(nil)
fmt.Println(a, len(a), cap(a)) // [] 0 0
if a == nil {
fmt.Println("a") // a
}
//a[0] = 1 // 在nil的slice中用索引赋值时会发生异常 panic: index out of range [0] with length 0
//fmt.Println(a[0]) // 在nil的slice中用索引赋值(或者访问)时会发生异常 panic: index out of range [0] with length 0
a = append(a, 1) // success, 自动扩容
fmt.Println(a, len(a), cap(a)) // [1] 1 1
// nil map
var b map[int]int // 也可写作: b := (map[int]int)(nil)
if b == nil {
fmt.Println("b") // b
}
x := b[1] // 从nil的map取数据, 返回默认的零值
fmt.Println(x) // 0
//b[1] = 1 // 直接对nil的map赋值会异常, panic: assignment to entry in nil map
// nil chan
var c chan int // 也可写作: c := (chan int)(nil)
if c == nil {
fmt.Println("c") // c
}
//c <- 1 // 从nil的chan写入或者读取时, 会一直阻塞造成死锁 fatal error: all goroutines are asleep - deadlock!
//close(c) // 关闭nil的chan时, 会发生异常, panic: close of nil channel
// nil struct指针
var d *struct{ x int } // 也可写作: d := (*struct{ x int })(nil)
if d == nil {
fmt.Println("d") // d
}
//d.x = 1 // d是空指针, 访问结构体的字段(或者赋值时)会异常, panic: runtime error: invalid memory address or nil pointer dereference
//fmt.Println(d.x) // d是空指针, 访问结构体的字段(或者赋值时)会异常, panic: runtime error: invalid memory address or nil pointer dereference
// nil interface
var e interface{} // 也可写作: e := (interface{})(nil)
if e == nil {
fmt.Println("e") // e
}
// nil func
var f func() // 也可写作: f := (func())(nil)
if f == nil {
fmt.Println("f") // f
}
}
将 Mutex 作为匿名字段时可能会导致锁机制失效
type data struct {
sync.Mutex
}
func (d data) test(s string) {
d.Lock()
defer d.Unlock()
for i:=0;i<5 ;i++ {
fmt.Println(s,i)
time.Sleep(time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
var d data
go func() {
defer wg.Done()
d.test("read")
}()
go func() {
defer wg.Done()
d.test("write")
}()
wg.Wait()
}
锁失效。将 Mutex 作为匿名字段时,相关的方法必须使用指针接收者,否则会导致锁机制失效。
闭包引用的是相同的变量
func test(x int) (func(), func()) {
// 函数中返回两个函数, 两个函数都用到了x变量
return func() {
println(x)
x += 10
}, func() {
println(x)
}
}
func main() {
a, b := test(100)
a()
b()
}
//打印
100
110
在闭包中引用的是相同的变量, 因此第一个a函数操作x后, 会影响第二个b函数中的x
数据的比较操作(==号)
golang四种数据类型
1.基本类型: 整形(int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune), 浮点型(float32/float64), 复数类型(complex64/complex128), 字符串(string)
2.复合类型(聚合类型): 数组(array), 结构体(struct)
3.引用类型: 切片(slice), map, channel, 指针
4.接口类型: interface, 如error
比较操作的前提是: 两个操作数的类型必须相同, go语言中比较时不会发生隐式转换, 必须显示指定类型比较
基本类型的比较
-
int8和int16是不同的类型, 其他类型中数字不同的同理为不同类型
-
自定义类型和基础类型是不同的类型, 如自定义类型myInt(定义为
type myInt int
)就不能和int类型比 -
但类型的别名和基础类型是属于同一个类型, 可以比较, 如byte和rune分别是uint8和int32的别名(定义为
type rune = int32
), byte可以和uint8比较再比如自定义一个别名myInt2(定义为
type myInt2 = int
, 别名的定义比自定义类型定义多加了一个等号), myInt2也可以和int比较
数组array的比较
- 数组的长度也属于类型的一部分, 不同长度的数字间不能比较
- 相同类型的数组间比较时, 会依次比较各个元素的值, 根据元素类型的不同, 按元素具体类型的比较规则进行比较
结构体struct的比较
- 结构体的定义完全一样(字段类型必须相同, 字段顺序必须相同)才认为是相同类型, 可以比较
- 相同结构体比较时, 按字段具体类型的比较规则进行比较
引用类型的比较
-
引用类型是间接指向它所引用的数据的,保存的是数据的地址。引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。 如:
type A struct{ a int } aa := &A{a: 1} bb := &A{a: 1} cc := aa fmt.Println(aa==bb) //false,虽然结构体定义相同,指向的结构体对象值也想同,但指向的对象地址不同 fmt.Println(aa==cc) //true,指向的地址相同
-
channel比较同理, 如:
aa := make(chan int, 1) bb := make(chan int, 2) cc := aa fmt.Println(aa==bb) //false,虽然chan定义相同,但指向的chan地址不同 fmt.Println(aa==cc) //true,指向的地址相同
-
slice之前不能比较, map之间也不能比较, 它们只能和nil比较
var a []int var b []int // invalid operation: a == b (slice can only be compared to nil) fmt.Println(a == b)
接口类型的比较
-
接口类型在比较时, 分为动态类型和动态值, 只有两者都完全相同, 才认为两个接口值是相等的
// 动态类型为基本类型 var a interface{} = 1 var b interface{} = 2 var c interface{} = 1 var d interface{} = 1.0 fmt.Println(a == b) // false, 动态类型相同, 动态值不同 fmt.Println(a == c) // true, 动态类型和动态值都相同 fmt.Println(a == d) // false, 动态类型不同 type A struct { a int b string } // 动态类型为struct var aa interface{} = A { a: 1, b: "test" } var bb interface{} = A { a: 1, b: "test" } var cc interface{} = A { a: 2, b: "test" } fmt.Println(aa == bb) // true, 动态类型和动态值都相同 fmt.Println(aa == cc) // false, 动态类型相同, 动态值不同 // 动态类型为指针 var dd interface{} = &A { a: 1, b: "test" } var ee interface{} = &A { a: 1, b: "test" } fmt.Println(dd == ee) // false, 动态类型相同, 动态值不同
-
若动态类型不可比较, 则接口类型也不可比较, 如动态类型为slice
var a interface{} = []int{1, 2, 3, 4} var b interface{} = []int{1, 2, 3, 4} // panic: runtime error: comparing uncomparable type []int fmt.Println(a == b)
-
接口类型比较时不要求接口自身的类型(注意不是动态类型)完全相同, 只要一个接口可以转化为另一个就可以比较, 如
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // invalid operation: r == w (mismatched types io.Reader and io.Writer) fmt.Println(r == w)
io.ReadCloser
可转化为io.Reader
,故两者可比较。而
io.Writer
不可转化为io.Reader
,编译报错。