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定义时省略的jb的长度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语言中比较时不会发生隐式转换, 必须显示指定类型比较

基本类型的比较

  1. int8和int16是不同的类型, 其他类型中数字不同的同理为不同类型

  2. 自定义类型和基础类型是不同的类型, 如自定义类型myInt(定义为type myInt int)就不能和int类型比

  3. 但类型的别名和基础类型是属于同一个类型, 可以比较, 如byte和rune分别是uint8和int32的别名(定义为type rune = int32), byte可以和uint8比较

    再比如自定义一个别名myInt2(定义为type myInt2 = int, 别名的定义比自定义类型定义多加了一个等号), myInt2也可以和int比较

数组array的比较

  1. 数组的长度也属于类型的一部分, 不同长度的数字间不能比较
  2. 相同类型的数组间比较时, 会依次比较各个元素的值, 根据元素类型的不同, 按元素具体类型的比较规则进行比较

结构体struct的比较

  1. 结构体的定义完全一样(字段类型必须相同, 字段顺序必须相同)才认为是相同类型, 可以比较
  2. 相同结构体比较时, 按字段具体类型的比较规则进行比较

引用类型的比较

  1. 引用类型是间接指向它所引用的数据的,保存的是数据的地址。引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。 如:

    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,指向的地址相同
    
  2. 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,指向的地址相同
    
  3. slice之前不能比较, map之间也不能比较, 它们只能和nil比较

    var a []int
    var b []int
    
    // invalid operation: a == b (slice can only be compared to nil)
    fmt.Println(a == b)
    

接口类型的比较

  1. 接口类型在比较时, 分为动态类型动态值, 只有两者都完全相同, 才认为两个接口值是相等的

    // 动态类型为基本类型
    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, 动态类型相同, 动态值不同
    
  2. 若动态类型不可比较, 则接口类型也不可比较, 如动态类型为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)
    
  3. 接口类型比较时不要求接口自身的类型(注意不是动态类型)完全相同, 只要一个接口可以转化为另一个就可以比较, 如

    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,编译报错。

posted @ 2023-02-13 21:38  Alex-GCX  阅读(195)  评论(0编辑  收藏  举报