• Golang那些违背直觉的编程陷阱


    目录

    知识点1:切片拷贝之后都是同一个元素

    知识点2:方法集合决定接口实现,类型方法集合是接口方法集合的超集则认定为实现接口,否则未实现接口


    切片拷贝之后都是同一个元素
    1. package main
    2. import (
    3. "encoding/json"
    4. "fmt"
    5. )
    6. func arr1() {
    7. var nn []int
    8. for i := 0; i < 5; i++ {
    9. nn = append(nn, i)
    10. }
    11. marshal, _ := json.Marshal(nn)
    12. // [0,1,2,3,4]
    13. fmt.Println(string(marshal))
    14. }
    15. func arr2() {
    16. var nn []*int
    17. for i := 0; i < 5; i++ {
    18. // 出错原因:每次都是i的地址,i的地址始终是一个,所以最终数组元素是5
    19. // 解决方法,每次新生命一个变量,之后使用每次新分配的变量进行赋值。参见arr3
    20. nn = append(nn, &i)
    21. }
    22. //[5,5,5,5,5]
    23. marshal, _ := json.Marshal(nn)
    24. fmt.Println(string(marshal))
    25. }
    26. func arr3() {
    27. var nn []*int
    28. for i := 0; i < 5; i++ {
    29. //
    30. s := i
    31. nn = append(nn, &s)
    32. }
    33. marshal, _ := json.Marshal(nn)
    34. //[0,1,2,3,4]
    35. fmt.Println(string(marshal))
    36. }
    37. func main() {
    38. arr1()
    39. arr2()
    40. arr3()
    41. }

    主要看一下arr2与arr3函数即可知晓,很好理解却又很容易疏忽。接下来看一个类似问题的变种,跟struct方法有关系示例:

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. type field struct {
    7. name string
    8. }
    9. func (p *field) print() {
    10. fmt.Println(p.name)
    11. }
    12. func main() {
    13. data1 := []*field{{"one"}, {"two"}, {"three"}}
    14. for _, v := range data1 {
    15. go v.print()
    16. }
    17. data2 := []field{{"four"}, {"five"}, {"six"}}
    18. for _, v := range data2 {
    19. go v.print()
    20. }
    21. time.Sleep(3 * time.Second)
    22. }

    | 这个代码执行输出结果:

    看到结果是不是很意外,为什么有3个six呢?接下来分下一下:由于field的print方法是指针类型,所以data2每次在调用print方法时都是v指向的内存对象,这个对象最后一次赋值是six,所以输出的是3个six(其实此处存在不确定性,main协程与子协程的调度顺序,如果每次调度main协程之后立马就去调度子协程可能结果就是正确的了)。

    那怎么修复问题呢?

    方法1:

    将filed的print方法的接受者修改为值类型,这样每次调用时都会拷贝一个副本进行调用,就会背面这个问题了,具体如下:

    1. func (p field) print() {
    2. fmt.Println(p.name)
    3. }

    方法2:

    每次调用时重新声明一个变量进行调用,这个底层原理也是拷贝一个副本进行调用,具体修改如下:

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. type field struct {
    7. name string
    8. }
    9. func (p *field) print() {
    10. fmt.Println(p.name)
    11. }
    12. func main() {
    13. data1 := []*field{{"one"}, {"two"}, {"three"}}
    14. for _, v := range data1 {
    15. go v.print()
    16. }
    17. data2 := []field{{"four"}, {"five"}, {"six"}}
    18. for _, v := range data2 {
    19. replica := v
    20. // 此处每次都是重新分配一个内存存储v的副本
    21. go replica.print()
    22. }
    23. time.Sleep(3 * time.Second)
    24. }
    方法集合决定接口实现,类型方法集合是接口方法集合的超集则认定为实现接口,否则未实现接口
    1. package main
    2. import (
    3. "fmt"
    4. "reflect"
    5. )
    6. type Interface interface {
    7. M1()
    8. M2()
    9. }
    10. type T struct{}
    11. func (t T) M1() {}
    12. func (t *T) M2() {}
    13. func DumpMethodSet(i interface{}) {
    14. v := reflect.TypeOf(i)
    15. elemTyp := v.Elem()
    16. n := elemTyp.NumMethod()
    17. if n == 0 {
    18. fmt.Printf("%s's method set is empty!\n", elemTyp)
    19. return
    20. }
    21. fmt.Printf("%s's method set:\n", elemTyp)
    22. for j := 0; j < n; j++ {
    23. fmt.Println("-", elemTyp.Method(j).Name)
    24. }
    25. fmt.Printf("\n")
    26. }
    27. func main() {
    28. var t T
    29. var pt *T
    30. var i Interface
    31. //Cannot use 't' (type T) as type Interface
    32. //Type does not implement 'Interface' as 'M2' method has a pointer receiver
    33. // 言外之意就是类型T没有实现接口的M2方法
    34. i = t
    35. i = pt
    36. }

    此处主要需要了解Go方法集合规范是什么才能更好解释问题。如下工具方法可以用于查看类型的方法集合,具体代码如下:

    1. func DumpMethodSet(i interface{}) {
    2. v := reflect.TypeOf(i)
    3. elemTyp := v.Elem()
    4. n := elemTyp.NumMethod()
    5. if n == 0 {
    6. fmt.Printf("%s's method set is empty!\n", elemTyp)
    7. return
    8. }
    9. fmt.Printf("%s's method set:\n", elemTyp)
    10. for j := 0; j < n; j++ {
    11. fmt.Println("-", elemTyp.Method(j).Name)
    12. }
    13. fmt.Printf("\n")
    14. }

    调用:

    1. var t T
    2. var pt *T
    3. DumpMethodSet(&t)
    4. DumpMethodSet(&pt)
    5. DumpMethodSet((*Interface)(nil))

    输出:

    因为T类型的方法集合只有M1,所以导致上面将T类型实例赋值给接口类型会报错。

    重点:Golang方法集合规范

    1. 对于非接口类型的自定义类型T,其方法集合由所有receiver为T类型的方法组成;

    2. 而类型*T的方法集合则包含所有receiver为T和*T类型的方法。也正因为如此,pt才能成功赋值给Interface类型变量。

    特别提示:在进行组合时候,内嵌的是指针或值类型的结构体所以涉及引入的方法集是不一样的,也遵循上面规范。一般来说内嵌指针的方法集大于等于值得方法集。参见代码:

    1. package main
    2. import "51788.net/golang-day01/dump_method_set"
    3. //main.T1's method set:
    4. //- T1M1
    5. //- T1M2
    6. //*main.T1's method set:
    7. //- PT1M3
    8. //- T1M1
    9. //- T1M2
    10. type T1 struct{}
    11. func (T1) T1M1() { println("T1's M1") }
    12. func (T1) T1M2() { println("T1's M2") }
    13. func (*T1) PT1M3() { println("PT1's M3") }
    14. //main.T2's method set:
    15. //- T2M1
    16. //- T2M2
    17. //
    18. //*main.T2's method set:
    19. //- PT2M3
    20. //- T2M1
    21. //- T2M2
    22. type T2 struct{}
    23. func (T2) T2M1() { println("T2's M1") }
    24. func (T2) T2M2() { println("T2's M2") }
    25. func (*T2) PT2M3() { println("PT2's M3") }
    26. //main.T's method set:
    27. //- PT2M3
    28. //- T1M1
    29. //- T1M2
    30. //- T2M1
    31. //- T2M2
    32. //
    33. //*main.T's method set:
    34. //- PT1M3
    35. //- PT2M3
    36. //- T1M1
    37. //- T1M2
    38. //- T2M1
    39. //- T2M2
    40. type T struct {
    41. T1
    42. *T2
    43. }
    44. func main() {
    45. t := T{
    46. T1: T1{},
    47. T2: &T2{},
    48. }
    49. pt := &t
    50. var t1 T1
    51. var pt1 *T1
    52. dump_method_set.DumpMethodSet(&t1)
    53. dump_method_set.DumpMethodSet(&pt1)
    54. var t2 T2
    55. var pt2 *T2
    56. dump_method_set.DumpMethodSet(&t2)
    57. dump_method_set.DumpMethodSet(&pt2)
    58. dump_method_set.DumpMethodSet(&t)
    59. dump_method_set.DumpMethodSet(&pt)
    60. }

    结论:

    • T类型的方法集合 = T1的方法集合 + *T2的方法集合;
    • *T类型的方法集合 = *T1的方法集合 + *T2的方法集合。
    接口方法覆盖
    1. package main
    2. import "51788.net/golang-day01/dump_method_set"
    3. type Interface1 interface {
    4. M1()
    5. }
    6. type Interface2 interface {
    7. M1()
    8. M2()
    9. }
    10. type Interface3 interface {
    11. Interface1
    12. Interface2 // Go 1.14之前版本报错:duplicate method M1
    13. }
    14. type Interface4 interface {
    15. Interface2
    16. M2() // Go 1.14之前版本报错:duplicate method M2
    17. }
    18. func main() {
    19. dump_method_set.DumpMethodSet((*Interface3)(nil))
    20. }

    在golang1.14版本之后允许接口中相同方法的覆盖。

    类型里面内嵌多个接口,多个接口方法集合存在交集

    当多个接口方法存在交集时,交集方法必须在类型上进行显示实现,否则调用交集方法时会报错。(当然如果不显示实现,而且后续不调用交集方法的话也不会报错。如果使用交集方法就要一定在类型上实现交集方法)。

    示例1:

    1. package main
    2. import "fmt"
    3. type IRun1 interface {
    4. M1()
    5. M2()
    6. }
    7. type IRun2 interface {
    8. M2()
    9. M3()
    10. }
    11. type IRun1Impl struct{}
    12. func (IRun1Impl) M1() {
    13. fmt.Println(" (IRun1Impl) M1()")
    14. }
    15. func (IRun1Impl) M2() {
    16. fmt.Println(" (IRun1Impl) M2()")
    17. }
    18. type IRun2Impl struct{}
    19. func (IRun2Impl) M2() {
    20. fmt.Println(" (IRun2Impl) M2()")
    21. }
    22. func (IRun2Impl) M3() {
    23. fmt.Println(" (IRun2Impl) M3()")
    24. }
    25. type TRun struct {
    26. IRun1
    27. IRun2
    28. }
    29. func (e TRun) M1() {
    30. fmt.Println("t m1")
    31. }
    32. // 一定在类型上实现交集方法
    33. func (e TRun) M2() {
    34. fmt.Println("t m2")
    35. }
    36. func main() {
    37. e := TRun{
    38. IRun1: &IRun1Impl{},
    39. IRun2: &IRun2Impl{},
    40. }
    41. e.M1()
    42. e.M2()
    43. e.M3()
    44. // 输出:
    45. //t m1
    46. //t m2
    47. // (IRun2Impl) M3()
    48. }

    示例2:

    1. package main
    2. import "fmt"
    3. type IRun1 interface {
    4. M1()
    5. M2()
    6. }
    7. type IRun2 interface {
    8. M2()
    9. M3()
    10. }
    11. type IRun1Impl struct{}
    12. func (IRun1Impl) M1() {
    13. fmt.Println(" (IRun1Impl) M1()")
    14. }
    15. func (IRun1Impl) M2() {
    16. fmt.Println(" (IRun1Impl) M2()")
    17. }
    18. type IRun2Impl struct{}
    19. func (IRun2Impl) M2() {
    20. fmt.Println(" (IRun2Impl) M2()")
    21. }
    22. func (IRun2Impl) M3() {
    23. fmt.Println(" (IRun2Impl) M3()")
    24. }
    25. type TRun struct {
    26. IRun1
    27. IRun2
    28. }
    29. func (e TRun) M1() {
    30. fmt.Println("t m1")
    31. }
    32. func main() {
    33. e := TRun{
    34. IRun1: &IRun1Impl{},
    35. IRun2: &IRun2Impl{},
    36. }
    37. e.M1()
    38. // 不在类型上声明M2方法,虽然两个接口都有声明M2方法,但是也会报错:
    39. // 编译器报错:Ambiguous reference 'M2'
    40. e.M2()
    41. e.M3()
    42. }

    示例三:

    1. package main
    2. import "fmt"
    3. type IRun1 interface {
    4. M1()
    5. M2()
    6. }
    7. type IRun2 interface {
    8. M2()
    9. M3()
    10. }
    11. type IRun1Impl struct{}
    12. func (IRun1Impl) M1() {
    13. fmt.Println(" (IRun1Impl) M1()")
    14. }
    15. func (IRun1Impl) M2() {
    16. fmt.Println(" (IRun1Impl) M2()")
    17. }
    18. type IRun2Impl struct{}
    19. func (IRun2Impl) M2() {
    20. fmt.Println(" (IRun2Impl) M2()")
    21. }
    22. func (IRun2Impl) M3() {
    23. fmt.Println(" (IRun2Impl) M3()")
    24. }
    25. type TRun struct {
    26. IRun1
    27. IRun2
    28. }
    29. func (e TRun) M1() {
    30. fmt.Println("t m1")
    31. }
    32. func main() {
    33. e := TRun{
    34. IRun1: &IRun1Impl{},
    35. IRun2: &IRun2Impl{},
    36. }
    37. e.M1()
    38. // 虽然没有在类型上声明M2方法,但是不调用M2方法的话也不会存在编译错误
    39. // 满足原则:你用你写,不用不写(u can u up)
    40. //e.M2()
    41. e.M3()
    42. }

    小提示:现实中应该避免这种复杂编程,显然无疑的提高了问题复杂度,并无显著收益。

    类型里面内嵌接口
    1. type InterfaceX interface {
    2. M1()
    3. M2()
    4. }
    5. type TS struct {
    6. InterfaceX
    7. }
    8. func (TS) M3() {}

    类型TS内嵌接口InterfaceX是允许的,而且编译器不要求强制必须实现M1与M2方法,这个如果有Java经验的话会很违背经验。但是Golang就是允许的,但是如果你调用未实现的方法就会报错:

    1. func main() {
    2. var t TS
    3. t.M1()
    4. }

    查看一下方法集合:

    1. func main() {
    2. dump_method_set.DumpMethodSet((*InterfaceX)(nil))
    3. var t TS
    4. var pt *TS
    5. dump_method_set.DumpMethodSet(&t)
    6. dump_method_set.DumpMethodSet(&pt)
    7. }

    输出:

    类型中内嵌接口,命名冲突的方法调用优先级
    1. package main
    2. type Interface interface {
    3. M1()
    4. M2()
    5. }
    6. type T struct {
    7. Interface
    8. }
    9. // 类型T上实现了接口M1方法,但是类型T未实现M2方法
    10. func (T) M1() {
    11. println("T's M1")
    12. }
    13. type S struct{}
    14. func (S) M1() {
    15. println("S's M1")
    16. }
    17. func (S) M2() {
    18. println("S's M2")
    19. }
    20. func main() {
    21. var t = T{
    22. Interface: S{},
    23. }
    24. // 因为类型实现了M1方法,所以直接调用M1的方法
    25. t.M1()
    26. // 因为接口类型没有实现M2方法,所以调用会从内嵌的接口上寻找方法
    27. t.M2()
    28. }

  • 相关阅读:
    自动驾驶:2022 apollo day 观后感(三)
    springboot集成logback打印彩色日志
    低代码助力中小企业数字化
    2023最新UI工作室官网个人主页源码/背景音乐/随机壁纸/一言
    单链表的定义— 不带头结点 + 带头结点
    高性能 MySQL(十二):分区表
    oracle行转列、列转行总结
    顾客为什么不到店了?
    如何快速开发app?小程序+插件少不了
    leetcode 20. 有效的括号
  • 原文地址:https://blog.csdn.net/Dax1n/article/details/138025773