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

目录

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

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


切片拷贝之后都是同一个元素
Go 复制代码
package main

import (
	"encoding/json"
	"fmt"
)

func arr1() {
	var nn []int
	for i := 0; i < 5; i++ {
		nn = append(nn, i)
	}
	marshal, _ := json.Marshal(nn)
	// [0,1,2,3,4]
	fmt.Println(string(marshal))
}

func arr2() {
	var nn []*int
	for i := 0; i < 5; i++ {
        // 出错原因:每次都是i的地址,i的地址始终是一个,所以最终数组元素是5
        // 解决方法,每次新生命一个变量,之后使用每次新分配的变量进行赋值。参见arr3
		nn = append(nn, &i)
	}
	//[5,5,5,5,5]
	marshal, _ := json.Marshal(nn)
	fmt.Println(string(marshal))
}

func arr3() {
	var nn []*int
	for i := 0; i < 5; i++ {
        // 
		s := i
		nn = append(nn, &s)
	}
	marshal, _ := json.Marshal(nn)
	//[0,1,2,3,4]
	fmt.Println(string(marshal))
}

func main() {
	arr1()
	arr2()
	arr3()
}

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

Go 复制代码
package main

import (
	"fmt"
	"time"
)

type field struct {
	name string
}

func (p *field) print() {
	fmt.Println(p.name)
}

func main() {

	data1 := []*field{{"one"}, {"two"}, {"three"}}
	for _, v := range data1 {
		go v.print()
	}

	data2 := []field{{"four"}, {"five"}, {"six"}}
	for _, v := range data2 {
		go v.print()
	}


	time.Sleep(3 * time.Second)
}

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

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

那怎么修复问题呢?

方法1:

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

Go 复制代码
func (p field) print() {
	fmt.Println(p.name)
}

方法2:

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

Go 复制代码
package main

import (
	"fmt"
	"time"
)

type field struct {
	name string
}

func (p *field) print() {
	fmt.Println(p.name)
}

func main() {

	data1 := []*field{{"one"}, {"two"}, {"three"}}
	for _, v := range data1 {
		go v.print()
	}

	data2 := []field{{"four"}, {"five"}, {"six"}}
	for _, v := range data2 {
		replica := v
        // 此处每次都是重新分配一个内存存储v的副本
		go replica.print()
	}

	time.Sleep(3 * time.Second)
}
方法集合决定接口实现,类型方法集合是接口方法集合的超集则认定为实现接口,否则未实现接口
Go 复制代码
package main

import (
	"fmt"
	"reflect"
)

type Interface interface {
	M1()
	M2()
}

type T struct{}

func (t T) M1()  {}
func (t *T) M2() {}

func DumpMethodSet(i interface{}) {
	v := reflect.TypeOf(i)
	elemTyp := v.Elem()

	n := elemTyp.NumMethod()
	if n == 0 {
		fmt.Printf("%s's method set is empty!\n", elemTyp)
		return
	}

	fmt.Printf("%s's method set:\n", elemTyp)
	for j := 0; j < n; j++ {
		fmt.Println("-", elemTyp.Method(j).Name)
	}
	fmt.Printf("\n")
}
func main() {
	var t T
	var pt *T

	var i Interface
	//Cannot use 't' (type T) as type Interface
	//Type does not implement 'Interface' as 'M2' method has a pointer receiver
	// 言外之意就是类型T没有实现接口的M2方法
	i = t
	i = pt
}

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

Go 复制代码
func DumpMethodSet(i interface{}) {
	v := reflect.TypeOf(i)
	elemTyp := v.Elem()

	n := elemTyp.NumMethod()
	if n == 0 {
		fmt.Printf("%s's method set is empty!\n", elemTyp)
		return
	}

	fmt.Printf("%s's method set:\n", elemTyp)
	for j := 0; j < n; j++ {
		fmt.Println("-", elemTyp.Method(j).Name)
	}
	fmt.Printf("\n")
}

调用:

Go 复制代码
var t T
var pt *T
DumpMethodSet(&t)
DumpMethodSet(&pt)
DumpMethodSet((*Interface)(nil))

输出:

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

重点:Golang方法集合规范

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

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

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

Go 复制代码
package main

import "51788.net/golang-day01/dump_method_set"

//main.T1's method set:
//- T1M1
//- T1M2

//*main.T1's method set:
//- PT1M3
//- T1M1
//- T1M2
type T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (T1) T1M2()   { println("T1's M2") }
func (*T1) PT1M3() { println("PT1's M3") }

//main.T2's method set:
//- T2M1
//- T2M2
//
//*main.T2's method set:
//- PT2M3
//- T2M1
//- T2M2
type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (T2) T2M2()   { println("T2's M2") }
func (*T2) PT2M3() { println("PT2's M3") }

//main.T's method set:
//- PT2M3
//- T1M1
//- T1M2
//- T2M1
//- T2M2
//
//*main.T's method set:
//- PT1M3
//- PT2M3
//- T1M1
//- T1M2
//- T2M1
//- T2M2
type T struct {
	T1
	*T2
}

func main() {
	t := T{
		T1: T1{},
		T2: &T2{},
	}
	pt := &t

	var t1 T1
	var pt1 *T1
	dump_method_set.DumpMethodSet(&t1)
	dump_method_set.DumpMethodSet(&pt1)

	var t2 T2
	var pt2 *T2
	dump_method_set.DumpMethodSet(&t2)
	dump_method_set.DumpMethodSet(&pt2)

	dump_method_set.DumpMethodSet(&t)
	dump_method_set.DumpMethodSet(&pt)
}

结论:

  • T类型的方法集合 = T1的方法集合 + *T2的方法集合;
  • *T类型的方法集合 = *T1的方法集合 + *T2的方法集合。
接口方法覆盖
Go 复制代码
package main

import "51788.net/golang-day01/dump_method_set"

type Interface1 interface {
	M1()
}

type Interface2 interface {
	M1()
	M2()
}

type Interface3 interface {
	Interface1
	Interface2 // Go 1.14之前版本报错:duplicate method M1
}

type Interface4 interface {
	Interface2
	M2() // Go 1.14之前版本报错:duplicate method M2
}

func main() {
	dump_method_set.DumpMethodSet((*Interface3)(nil))
}

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

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

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

示例1:

Go 复制代码
package main

import "fmt"

type IRun1 interface {
	M1()
	M2()
}

type IRun2 interface {
	M2()
	M3()
}

type IRun1Impl struct{}

func (IRun1Impl) M1() {
	fmt.Println(" (IRun1Impl) M1()")
}

func (IRun1Impl) M2() {
	fmt.Println(" (IRun1Impl) M2()")
}

type IRun2Impl struct{}

func (IRun2Impl) M2() {
	fmt.Println(" (IRun2Impl) M2()")
}

func (IRun2Impl) M3() {
	fmt.Println(" (IRun2Impl) M3()")
}

type TRun struct {
	IRun1
	IRun2
}

func (e TRun) M1() {
	fmt.Println("t m1")
}

// 一定在类型上实现交集方法
func (e TRun) M2() {
	fmt.Println("t m2")
}

func main() {
	e := TRun{
		IRun1: &IRun1Impl{},
		IRun2: &IRun2Impl{},
	}
	e.M1()
	e.M2()
	e.M3()
	//	输出:
	//t m1
	//t m2
	// (IRun2Impl) M3()
}

示例2:

Go 复制代码
package main

import "fmt"

type IRun1 interface {
	M1()
	M2()
}

type IRun2 interface {
	M2()
	M3()
}

type IRun1Impl struct{}

func (IRun1Impl) M1() {
	fmt.Println(" (IRun1Impl) M1()")
}

func (IRun1Impl) M2() {
	fmt.Println(" (IRun1Impl) M2()")
}

type IRun2Impl struct{}

func (IRun2Impl) M2() {
	fmt.Println(" (IRun2Impl) M2()")
}

func (IRun2Impl) M3() {
	fmt.Println(" (IRun2Impl) M3()")
}

type TRun struct {
	IRun1
	IRun2
}

func (e TRun) M1() {
	fmt.Println("t m1")
}

func main() {
	e := TRun{
		IRun1: &IRun1Impl{},
		IRun2: &IRun2Impl{},
	}
	e.M1()
	// 不在类型上声明M2方法,虽然两个接口都有声明M2方法,但是也会报错:
	// 编译器报错:Ambiguous reference 'M2'
	e.M2()
	e.M3()

}

示例三:

Go 复制代码
package main

import "fmt"

type IRun1 interface {
	M1()
	M2()
}

type IRun2 interface {
	M2()
	M3()
}

type IRun1Impl struct{}

func (IRun1Impl) M1() {
	fmt.Println(" (IRun1Impl) M1()")
}

func (IRun1Impl) M2() {
	fmt.Println(" (IRun1Impl) M2()")
}

type IRun2Impl struct{}

func (IRun2Impl) M2() {
	fmt.Println(" (IRun2Impl) M2()")
}

func (IRun2Impl) M3() {
	fmt.Println(" (IRun2Impl) M3()")
}

type TRun struct {
	IRun1
	IRun2
}

func (e TRun) M1() {
	fmt.Println("t m1")
}

func main() {
	e := TRun{
		IRun1: &IRun1Impl{},
		IRun2: &IRun2Impl{},
	}
	e.M1()
	// 虽然没有在类型上声明M2方法,但是不调用M2方法的话也不会存在编译错误
	// 满足原则:你用你写,不用不写(u can u up)
	//e.M2()
	e.M3()
}

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

类型里面内嵌接口
Go 复制代码
type InterfaceX interface {
	M1()
	M2()
}

type TS struct {
	InterfaceX
}

func (TS) M3() {}

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

Go 复制代码
func main() {
	var t TS
	t.M1()
}

查看一下方法集合:

Go 复制代码
func main() {
	dump_method_set.DumpMethodSet((*InterfaceX)(nil))
	var t TS
	var pt *TS
	dump_method_set.DumpMethodSet(&t)
	dump_method_set.DumpMethodSet(&pt)
}

输出:

类型中内嵌接口,命名冲突的方法调用优先级
Go 复制代码
package main

type Interface interface {
	M1()
	M2()
}

type T struct {
	Interface
}

// 类型T上实现了接口M1方法,但是类型T未实现M2方法
func (T) M1() {
	println("T's M1")
}

type S struct{}

func (S) M1() {
	println("S's M1")
}
func (S) M2() {
	println("S's M2")
}

func main() {
	var t = T{
		Interface: S{},
	}
	// 因为类型实现了M1方法,所以直接调用M1的方法
	t.M1()
	// 因为接口类型没有实现M2方法,所以调用会从内嵌的接口上寻找方法
	t.M2()
}
相关推荐
智驭未来掌门人4 分钟前
利用FFmpeg快速实现一个RTSP推流服务器
后端
程序员雷欧12 分钟前
AQS深度解析
开发语言·python
垚垚学技术_聚焦云原生17 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide21 分钟前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_24 分钟前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐43 分钟前
安全地生成验证码、密码和 Token
后端
小强19881 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
yujunl1 小时前
总结一下U9客开的成果
开发语言