20小时快速入门go语言视频 - Day4

笔记声明

本人智商较低,好记性不如烂笔头。笔记记录了网上一套Golang免费视频教程的知识点,以供未来自己翻阅查看。不写源视频的名称和网站地址了,免得被说是广告!


20小时快速入门go语言视频 - Day4



一、Golang面向对象编程概念

传统编程语言的面向对象
在这里插入图片描述
Golang没有沿袭其他传统编程语言面向对象的概念:没有封装、继承、多态这些概念,但可以通过别的方式来实现这些特性:

  • 封装:通过方法实现
  • 继承:通过匿名字段实现
  • 多态:通过接口实现



二、匿名字段

2.1 匿名字段的概念

一般情况下,定义结构体的时候是字段名与其类型一一对应,实际上Golang支持只提供类型而不写字段名的方式,这就是匿名字段。
匿名字段的简单体验:
当匿名字段也是一个结构体s1且存在于另一个结构体s2的时候,那么s1结构体的全部字段都会被隐式地引入到结构体s2。此时,匿名字段也可以被称为嵌入字段,实现了"继承"的思想。
例如:

//人
type Person struct {
	name string
	sex  byte
	age  int
}

//学生
type Student struct {
	Person //匿名字段,只有Person类型而没有名字,那么Student默认就包含了Person的所有字段

	//学生额外的字段
	stuNo   int //学号
	address string
}

Student中的Person,Person只是个类型而没有给它起名称,所以Person就是匿名字段!Student中的Person会把Person类型中的字段全部引用过来。

2.2 匿名字段初始化

2.2.1 顺序初始化

对应结构体中的字段,一个个给值。
示例:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	var s1 = Student{
		Person:  Person{"go", 'M', 11}, //Person本身也是一个结构体,对结构体匿名字段进行初始化需要这样写
		stuNo:   0,
		address: "Google Inc.",
	}
	fmt.Println("s1=", s1) //s1= {{go 77 11} 0 Google Inc.}
}
2.2.2 自动推导类型

示例:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	s1 := Student{Person{"go", 'M', 11}, 0, "Google Inc."}
	fmt.Printf("s1=%+v\n", s1) //s1={Person:{name:go sex:77 age:11} stuNo:0 address:Google Inc.}
}

%+v 信息显示地更加详细!从上例中可以看到,每个字段名也打印了出来。
注意:如果结构体中使用了另一个结构体,就必须显示地带上类型,不然就报错!
例如:

func main() {
	s2 := Student{{"go", 'M', 11}, 0, ""} //报错:missing type in composite literal ===> 组合字面量中缺少类型
	fmt.Println("s2=", s2)
}

{"go", 'M', 11} 前面一定要带上 Person 这个类型!
匿名函数在Golang官方中的叫法是:函数字面量,这里再次看到了字面量这个词语,也可以得知匿名字段也只一个结构体的字面量。

2.2.3 指定成员初始化

指定结构体匿名字段的话,就一层套一层地去写。例如:StructName:StructName{FieldName:Value}

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	//一层套一层的写法,Person:Person{字段名:值}
	s1 := Student{Person: Person{name: "go"}, address: "Google Inc."}
	fmt.Printf("s1=%+v\n", s1) //s1={Person:{name:go sex:0 age:0} stuNo:0 address:Google Inc.}
}

2.3 成员操作

跟操作普通结构体的方式一模一样。

2.3.1 方式一

结构体字面量(结构体匿名字段)已经被直接引入了,可以用 . 来访问。
示例:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	s1 := Student{Person{"go", 'M', 11}, 0, ""}
	fmt.Printf("s1.name=%v\n", s1.name) //直接可以使用了
	fmt.Printf("s1.sex=%v\n", s1.sex)
	fmt.Printf("s1.age=%v\n", s1.age)
	fmt.Printf("s1.stuNo=%v\n", s1.stuNo)
	fmt.Printf("s1.address=%v\n", s1.address)
}

/*
s1.name=go
s1.sex=77
s1.age=11
s1.stuNo=0
s1.address=
*/
2.3.2 方式二
//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	s1 := Student{Person{"go", 'M', 11}, 0, ""}
	s1.name = "Golang" //修改成员的值,也是直接用点"."来操作
	s1.address = "Google Inc."
	fmt.Printf("s1=%+v\n", s1) //s1={Person:{name:Golang sex:77 age:11} stuNo:0 address:Google Inc.}
}
2.3.3 方式三

结构体中的字面量当做一个整体赋值。
示例:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string
}

func main() {
	s1 := Student{Person{"go", 'M', 11}, 0, ""}

	//Person在Student中是一个结构体,可以被整体赋值
	s1.Person = Person{name: "go-lang"} //注意等号"="后面要带上类型,未给值的将把原来的值给覆盖使用其类型的零值

	fmt.Printf("s1=%+v\n", s1) //s1={Person:{name:go-lang sex:0 age:0} stuNo:0 address:}
}

注意:当做一个整体赋值的时候,如果有字段没给值,那么就会使用其类型对应的零值覆盖原值

2.4 同名字段

实质上跟作用域的规则一样:能在当前作用域内找到的就用当前作用域内的字段,没有找到才往上去找。
例1:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string

	name string //和Person中的name同名了
}

func main() {
	var s Student
	s.name = "Golang"
	s.sex = 'M'
	s.age = 11
	fmt.Printf("s=%+v\n", s)
	//s={Person:{name: sex:77 age:11} stuNo:0 address: name:Golang}
}

可以看到Person中的name是空字符串,跟作用域的规则一样:s.name能在Student中找到,那么就用了Student中的name,不会去管Person中的name了;而Student中没有sex这个字段,那么程序就会往上去找,找到了Person中有sex字段,就给Person中的sex字段赋值了。
想要使用Person中的name,就需要显示调用
例2:

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person //结构体匿名字段,Student默认就包含了Person的所有字段

	stuNo   int //学号
	address string

	name string //和Person中的name同名了
}

func main() {
	var s Student
	s.name = "Golang"
	s.sex = 'M' //Student中没有sex这个字段,会往上去找,找到Person中有sex字段
	s.age = 11
	s.Person.name = "01" //显示调用,指定为Person中的name
	s.Person.sex = 'u' //指定调用,这里会覆盖之前的值
	s.Person.age = 219
	fmt.Printf("s=%+v\n", s)
	//s={Person:{name:01 sex:117 age:219} stuNo:0 address: name:Golang}
}

显示调用后,两个name都被指定了,就各管各的了。

2.5 非结构体匿名字段

就是只写数据类型而不写变量名。

type myAddress string //自定义类型,说白了就是给一个类型起个别名

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	Person    //结构体匿名字段
	int       //普通类型的匿名字段
	myAddress //使用自定义类型
}

func main() {
	s := Student{Person{"Go", 'M', 11}, 101010, "Google Inc."}
	fmt.Printf("s=%+v\n", s)

	s.Person = Person{name: "C", sex: 'm'} //当做一个整体赋值
	s.int = 999 //直接操作那个类型即可
	s.myAddress = "贝尔实验室" //直接操作结构体里的字段即可
	fmt.Printf("s=%+v\n", s)
}

/*
运行结果:
s={Person:{name:Go sex:77 age:11} int:101010 myAddress:Google Inc.}
s={Person:{name:C sex:109 age:0} int:999 myAddress:贝尔实验室}
*/

Student 中的 int 字段就是非结构体(普通类型)匿名字段。

2.6 结构体匿名字段的指针类型

示例:

type myAddress string //自定义类型,说白了就是给一个类型起个别名

//人
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//学生
type Student struct {
	*Person   //结构体匿名字段的指针类型
	int       //普通类型的匿名字段
	myAddress //使用自定义类型
}

func main() {
	//第一种方式,使用取地址符"&"
	//Student中的Person是指针类型,这里的Person需要在前面加上取地址符"&"
	s := Student{&Person{"Go", 'M', 11}, 101010, "Google Inc."}
	fmt.Printf("s=%+v\n", s) //&Person是取内存地址,这样写只会打印出内存地址的值
	//需要一个个写才能显示真正的内容
	fmt.Println(s.Person.name, s.Person.sex, s.Person.age, s.int, s.myAddress) //Go 77 11 101010 Google Inc.

	//第二种方式,new()
	var s2 Student
	s2.Person = new(Person) //分配一个内存地址,让*Person变成一个指向Person的合法指针
	//有合法指向后,即可操作里面的成员
	s2.name = "C"
	s2.sex = 'm'
	s2.age = 48
	s2.int = 999 //s2.int不是指针,只是普通类型的匿名字段
	s2.myAddress = "贝尔实验室"
	fmt.Println(s2.Person.name, s2.Person.sex, s2.Person.age, s2.int, s2.myAddress)
}

/*
运行结果:
s={Person:0xc0000044a0 int:101010 myAddress:Google Inc.}
Go 77 11 101010 Google Inc.
C 109 48 999 贝尔实验室
*/



三、方法

在 Golang 中,方法本质上也是一个函数,它要和自定义类型绑定在一起。绑定了某一个类型的函数,被称为方法。带有接收者的函数叫方法,换言之:为某个类型绑定了一个函数,那个函数就被称为方法。
可以给任意自定义类型添加相应的方法(指针和接口类型除外)。
语法:func (receiver ReceiverType) funcName(params) (results)
receiver:接收者的名称,可任意命名,符合变量命名规则即可。方法要跟自定义类型绑定在一起,被绑定的那个叫接收者。
ReceiverType:接收者的数据类型,可以是T或者*T,但它自身的基类型T不能是接口或指针。

3.1 基本数据类型绑定方法

示例:

//自定义一个类型
type long int

//给long类型绑定一个Add函数,由tmp来接收,tmp就是接收者
//接收者就是传递过来的一个参数,other是调用时小括号里的另一个参数
func (tmp long) Add(other long) long {
	return tmp + other
}

func main() {
	var a long //定义一个变量a,类型是long
	a = 10     //long指向的是int类型,所以可以直接给int类型的值,其他类型是不可以的

	//a是一个long类型,long类型绑定了一个Add函数,所以a可以调用long类型中的这个Add函数
	//这里的a其实是一个接收者,传递过去一个参数即可
	result := a.Add(20)

	//注意:Add返回的是一个long类型了
	fmt.Printf("result type is : %T, result=%v\n", result, result)
	//result type is : main.long, result=30
}

result := a.Add(20) 这行代码中,a 是一个 long 类型的变量,对应地传递给了 long 类型的接收者,20 传递给方法中的形参:
在这里插入图片描述

3.2 结构体类型绑定方法

3.2.1 示例:绑定一个方法,实现修改结构体成员值

注意:结构体是值传递,要修改结构体成员的值需要用到指针。
示例:

type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//带有接收者的函数被称为方法,使用类型来调用
//接收者本身就是一个参数
func (tmp Person) PrintInfo() {
	fmt.Println("person infos =", tmp)
}

//修改结构体的成员需要用到指针
func (p *Person) SetInfo(name string, sex byte, age int) {
	p.name = name
	(*p).sex = sex
	p.age = age
}

func main() {
	//自动推导
	p := Person{"go", 'm', 11}
	p.PrintInfo() //p本身就是一个参数,传递给tmp这个接收者

	//接收者是一个指针类型,需要一个指向合法内存地址的指针
	//new()方式
	p1 := new(Person)
	p1.SetInfo("C", 'M', 48)
	p1.PrintInfo()

	//取地址符方式
	var p2 Person
	(&p2).SetInfo("Ada", 'F', 40)
	p2.PrintInfo() //没有取地址符在这里也可以
}

/*
运行结果:
person infos = {go 109 11}
person infos = {C 77 48}
person infos = {Ada 70 40}
*/
3.2.2 针对上面例子的反面教材

如果不传递结构体的内存地址,结构体将以值传递的方式进行传参,不同作用域内的结构体是各管各的。
修改3.2.1的代码成如下:

type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

func (tmp Person) PrintInfo() {
	fmt.Println("person infos =", tmp)
}

//修改结构体的成员需要用到指针
//未使用指针,接收的是一个拷贝过来的副本,在函数中的任何操作都只是在操作副本
func (p Person) SetInfo(name string, sex byte, age int) {
	p.name = name
	p.sex = sex
	p.age = age
	fmt.Println("inner SetInfo():", p)
}

func main() {
	p1 := Person{"go", 'm', 11}
	fmt.Printf("main(), before setting ... ")
	p1.PrintInfo()

	p1.SetInfo("C", 'M', 48)
	fmt.Printf("main(), after setting ... ")
	p1.PrintInfo()
}

/*
运行结果:
main(), before setting ... person infos = {go 109 11}
inner SetInfo(): {C 77 48}
main(), after setting ... person infos = {go 109 11}
*/

结构体传参,默认是值传递:只是把一份副本拷贝过去。在SetInfo()函数中的所有操作都只是在操作这个副本,SetInfo()函数结束后,这个副本就被回收了。那么在main()中,是找不到这个副本的,所以依然会使用之前的。
在另外一个函数中想要修改结构体成员的值,必须传址!

3.3 注意事项

3.3.1 ReceiverType接收者的基类型不能是指针
type long int

//接收者不使用的话,可以省略不写
//没问题可以编译通过
func (long) Add(second int) {
}

//

type bigInt *int
//报错:invalid receiver type bigInt (bigInt is a pointer type)
//非法的接收者类型(bigInt是一个指针类型)
func (tmp bigInt) Add(second int) {
}

//这样是能编译通过的
//接收者接收一个内存地址,而long本身它是int类型,不是指针类型
func (tmp *long) SetValue(value int) {
}

func main() {
}

bigInt 是一个接收者类型,它本身是一个 int 指针,而** Golang 规定方法的接收者类型不允许为指针**!

3.3.2 接收者类型不一样,就不同的方法

下例不会出现重复定义的错误:

type char byte
type long int32

func (c char) test() {
}

func (l long) test() {
}

func main() {
}

在这里插入图片描述

3.4 值语义和引用语义

就是值传递与引用传递的区别,值传递称为值语义,引用传递称为引用语义。
示例:

type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

//值传递(值语义),传过来的是实参的一份副本
func (p Person) SetInfoValue(name string, sex byte, age int) {
	p.name = name
	p.sex = sex
	p.age = age

	fmt.Printf("SetInfoValue(): p address:%p, p=%v\n", &p, p) //这里的接收者不是一个指针类型,所以取地址要加上取地址符"&"
}

//引用传递(引用语义),接收一个内存地址的实参
func (p *Person) SetInfoPointer(name string, sex byte, age int) {
	p.name = name
	p.sex = sex
	p.age = age

	//这里的接收者本身就是一个指针,存放的就是内存地址,无需加取地址符
	//取值还是需要加上星花符"*"
	fmt.Printf("SetInfoPointer():p address:%p, p=%v\n", p, *p)
}

func main() {
	s := Person{"Go", 'M', 11}
	fmt.Printf("main(): s address:%p, s=%v\n", &s, s)

	//值语义
	s.SetInfoValue("C", 'M', 42)

	//引用语义,取出内存地址当做参数传递过去
	(&s).SetInfoPointer("C", 'M', 42)
}

/*
运行结果:
main(): s address:0xc0000044a0, s={Go 77 11}
SetInfoValue(): p address:0xc000004500, p={C 77 42}
SetInfoPointer():p address:0xc0000044a0, p={C 77 42}
*/

SetInfoValue() 它绑定的接收者是一个普通类型,结构体默认是值传递,所以得到的是一份实参的拷贝,进了这个函数中,main() 中的实参和 SetInfoValue() 中的实参,各管各的了,互不相关。
SetInfoPointer() 它绑定的接收者是一个指针类型,需要的是一个内存地址的实参。得到了变量的内存地址,那么无论哪里修改,都是在修改内存地址中的值,一处修改影响原本的实参。

3.5 方法集

类型的方法集是指:可以被该类型变量调用的所有方法的集合。
用一个变量(value/pointer)调用方法(含匿名字段)不受方法集约束,编译器总能够查找全部方法,并自动转换 receiver 实参。

3.5.1 指针变量的方法集

结构体变量是一个指针变量,它能够调用哪些方法,这些方法就是一个集合,简称:方法集。
示例:

type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

func (p Person) SetInfoValue() {
	fmt.Println("SetInfoValue")
}

func (p *Person) SetInfoPointer() {
	fmt.Println("SetInfoPointer")
}

func main() {
	//初始化一个指针变量
	p := &Person{"Golang", 'M', 11} //指针指向内存地址,所以用取地址符"&"

	p.SetInfoPointer() //指针变量理所当然能够调用指针接收者的方法

	//SetInfoValue()的接收者是一个普通类型变量,但p是一个指针类型变量。此时,Golang内部就会做自动转换
	//先把指针变量p,转换成(*p),然后再调用。实际上它的操作就是 (*p).SetInfoValue()
	//实参p是一个指针类型变量,但接收者是一个普通类型变量,此时Golang内部就会自动转换
	p.SetInfoValue() //经过内部转换,指针变量同时也能调用非指针接收者的方法
	
	//这里手动显示转换,取值符把指针变量变成了普通变量,接收者也是一个普通变量,Golang不会去自动转换了,略微提高了性能
	(*p).SetInfoValue()

	//手动把指针变量p,转换为普通变量p,也能调用
	//Golang内部会去再次做转换
	(*p).SetInfoPointer() //取值了,就变成了普通变量
}

/*
运行结果:
SetInfoPointer
SetInfoValue
SetInfoValue
SetInfoPointer
*/
3.5.2 普通变量的方法集
type Person struct {
	name string
	sex  byte //最终以ASCII码值的方式打印
	age  int
}

func (p Person) SetInfoValue() {
	fmt.Println("SetInfoValue")
}

func (p *Person) SetInfoPointer() {
	fmt.Println("SetInfoPointer")
}

func main() {
	//普通变量
	p := Person{"Golang", 'M', 11} //指针指向内存地址,所以用取地址符"&"

	//内部先把普通变量p,转换为&p后再调用
	//实际上它是 (&p).SetInfoPointer()
	p.SetInfoPointer()

	p.SetInfoValue() //p本身就是个普通变量,当然能掉普通接收者类型的方法
}

/*
运行结果:
SetInfoPointer
SetInfoValue
*/
3.5.3 方法集的总结

结构体变量要去调用方法,无需关心结构体变量是指针类型还是普通类型,Golang 非常智能,内部都会去做对应的转换,以适应最终的 ReceiverType

3.6 方法的匿名

使用了嵌入字段,同业也会将那个字段的方法引用过来。
示例:

type Person struct {
	name string
	sex  byte
	age  int
}

//给Person结构体绑定一个方法
func (p *Person) PrintInfo() {
	fmt.Printf("name=%s, sex=%c, age=%d\n", p.name, p.sex, p.age)
}

//Student引用Person结构体
type Student struct {
	Person  //匿名字段
	stuNo   int
	address string
}

func main() {
	s := Student{Person{"Go", 'M', 11}, 1010, "Google Inc."}

	//Student结构体引用Person,Person中有PrintInfo()方法,因此Student结构体变量也可以直接调用
	s.PrintInfo()
}

/*
运行结果:
name=Go, sex=M, age=11
*/

Student 有对 Person 结构体的引用,Person 结构体绑定了一个方法,既然 Student 引用了 Person 这个结构体,那么同时也会得到 Person 所有的属性和方法。

3.7 同名方法

在 3.3.2 中已经提及过这个概念,接收者不一样,就算方法名一样,那么也是两个不同的方法,只是看起来名字一样而已。查找规则跟作用域原则一致!
示例:

type Person struct {
	name string
	sex  byte
	age  int
}

func (p *Person) PrintInfo() {
	fmt.Printf("*Person=%+v\n", *p)
}

type Student struct {
	Person  //匿名字段
	stuNo   int
	address string
}

func (s *Student) PrintInfo() {
	fmt.Printf("*Student=%+v\n", *s)
}

func main() {
	s := Student{Person{"Go", 'M', 11}, 1010, "Google Inc."}

	//查找规则跟作用域原则的一样,先找自己本身
	s.PrintInfo()

	//只想调用Person的那个PrintInfo(),就需要显示调用
	s.Person.PrintInfo()
}

/*
运行结果:
*Student={Person:{name:Go sex:77 age:11} stuNo:1010 address:Google Inc.}
*Person={name:Go sex:77 age:11}
*/

3.8 方法值和方法表达式

3.8.1 方法值

本质上是方法的字面量,将方法的入口(可以简单理解成:函数体)赋值给一个变量,变量保存了这个方法的入口,可以直接使用变量名()方式调用。

type Person struct {
	name string
	sex  byte
	age  int
}

func (p Person) SetValue() {
	fmt.Printf("SetValue: p address=%p, p=%+v\n", &p, p) //普通类型变量,格式化地址值需要加取地址符"&"
}

func (p *Person) SetPointer() {
	fmt.Printf("SetPointer: p address=%p, p=%+v\n", p, p) //p本身就是指针类型
}

func main() {
	p := Person{"Go", 'M', 11}
	fmt.Printf("main: p address=%p, p=%+v\n", &p, p)

	p.SetPointer() //传统调用方式

	pFunc := p.SetPointer //将方法的入口赋值给变量,变量就保存了该方法。这个就是方法值,调用方法时无需再传接收者,因为已经隐藏了接收者
	pFunc()               //pFunc这个变量保存了一个方法,可以直接加圆括号调用。等价于 p.SetPointer()

	//普通数据类型使用方法值也是一样的操作
	vFunc := p.SetValue //将方法的入口赋值给一个变量,隐藏了接收者
	vFunc()             //等价于 p.SetValue()
}

/*
运行结果:
main: p address=0xc0000044a0, p={name:Go sex:77 age:11}
SetPointer: p address=0xc0000044a0, p=&{name:Go sex:77 age:11}
SetPointer: p address=0xc0000044a0, p=&{name:Go sex:77 age:11}
SetValue: p address=0xc000004540, p={name:Go sex:77 age:11}
*/
3.8.2 方法表达式
type Person struct {
	name string
	sex  byte
	age  int
}

func (p Person) SetValue() {
	fmt.Printf("SetValue: p address=%p, p=%+v\n", &p, p) //普通类型变量,格式化地址值需要加取地址符"&"
}

func (p *Person) SetPointer() {
	fmt.Printf("SetPointer: p address=%p, p=%+v\n", p, p) //p本身就是指针类型
}

func main() {
	p := Person{"Go", 'M', 11}
	fmt.Printf("main: p address=%p, p=%+v\n", &p, p)

	f1 := (*Person).SetPointer //接收者是指针类型,就需要加上星花符"*"
	f1(&p) //显示地把实参传递给接收者,等价于 p.SetPointer()

	f2 := (Person).SetValue //接收者是普通数据类型
	f2(p) //显示地把变量传递给接收者
}

/*
运行结果:
main: p address=0xc0000044a0, p={name:Go sex:77 age:11}
SetPointer: p address=0xc0000044a0, p=&{name:Go sex:77 age:11}
SetValue: p address=0xc000004520, p={name:Go sex:77 age:11}
*/
3.8.3 两者的区别

方法值把接收者的实参给隐藏了起来,方法表达式需要显示传参,只是写法上的不同而已。本质上都是保存了方法的入口,让变量变成一个方法字面量。



四、接口

接口实现了"多态"的思想,是一种用于表示其他类型的行为的数据类型。结构体里面放的是成员(属性)、变量,接口里面放的是方法(实际上就是函数)的声明,方法由其他类型实现。
接口类型只会展示出它们自己的方法,它不会暴露出它所代表的内部值的结构和支持的基础操作的集合。
Go 的接口实现了鸭子类型 duck-typing,我不关心这只动物到底是鸟还是鸭子还是鸵鸟,我只要看到它走起来像鸭子、游泳起来像鸭子或者叫起来像鸭子,那么这只动物就是鸭子。
总结:不关心数据类型,只关心行为。不关心最终由哪个类型来实现,只关心最终实现了什么行为!

4.1 接口声明

基本语法:

type Humaner interface {
	SayHi() ReturnType
}

注意事项:
1.接口命名习惯以 er 结尾。
2.接口只有方法声明,没有实现,没有数据字段。
3.接口可以匿名嵌入其他接口,或嵌入到结构中。
4.声明带返回值的方法时,把返回值类型写在方法名的小括号后面。

4.2 接口实现

接口是用来定义行为(方法)的类型。这些被定义的行为不由接口直接实现,而是由用户定义的类型实现。
如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋值给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
示例:

//定义接口类型
type Humaner interface {
	//方法的声明,没有实现,由别的类型(自定义类型)实现
	sayhi()
}

type Student struct {
	name  string
	stuNo int
}

//Student类型实现了sayhi()方法
func (s *Student) sayhi() {
	fmt.Printf("Student[%s, %d], say hi.\n", s.name, s.stuNo)
}

type Teacher struct {
	address string
	group   string
}

//Teacher类型实现了sayhi()方法
func (t *Teacher) sayhi() {
	fmt.Printf("Teacher[%s, %s], say hi.\n", t.address, t.group)
}

type MyStr string

func (s *MyStr) sayhi() {
	fmt.Printf("MyStr[%s], say hi.\n", *s) //取地址中的值,需要星花符"*"
}

func main() {
	//定义接口类型的变量
	var i Humaner //i的类型是Humaner接口

	//只要实现了此接口方法的类型,那么这个类型的变量(接收者类型)就可以赋值给接口的变量
	s := &Student{"Go", 1010} //接收者是指针类型,需要取出内存地址
	i = s
	i.sayhi()

	t := &Teacher{"Google Inc.", "Go"}
	i = t
	i.sayhi()

	//把接口类型赋值给自定义类型
	var str MyStr = "hello go" //需要显示写上MyStr类型,没有写,就会默认推导成字符串类型。字符串类型和MyStr类型,不是同一个类型
	i = &str
	i.sayhi()
}

/*
运行结果:
Student[Go, 1010], say hi.
Teacher[Google Inc., Go], say hi.
MyStr[hello go], say hi.
*/

同一个接口实现了不同的表现,就看给接口类型的变量,赋值了一个什么类型。
大致流程分析:
首先:声明了一个接口类型,这个接口类型中有一个叫 sayhi() 的方法。接口类型只有方法的声明,没有方法的实现以及其他任何属性、变量
接下来:每一个结构体都实现了该方法。以 func (s *Student) sayhi() {...} 为例,sayhi 中实现了一些行为(说白了就是写了些代码,要 *Student 这个类型干些什么事情),并绑定到了 *Student 类型中。
紧接着:定义了一个接口变量 i,&Student 赋值给了 i(因为 Student 的接收者类型是一个指针类型,所以需要传递一个内存地址给它)。调用 i.sayhi() 的时候,就会去找 &Student 中的那个 sayhi() 方法。
同理:Teacher 的地址重新赋值给了 i,再次调用 i.sayhi() 的时候,就会去找 &Teacher 中的那个 sayhi() 方法。
最后:自定义类型 MyStr 的变量 &str 赋值给了i,调用的时候就会去找 &MyStr 中的那个 sayhi() 方法。注意:MyStr 一定要显示写法,不写的话,str 就会被自动推导成为 string 类型。string 类和 MyStr 类型,根本不是同一个数据类型,肯定会报错!
总结:就看给接口变量赋值了什么类型的变量,根据赋值的变量类型,自动选择赋值变量类型所匹配的方法。

4.3 调用同一个函数,实现不同行为

示例:

//定义接口类型
type Humaner interface {
	//方法的声明,没有实现,由别的类型(自定义类型)实现
	sayhi()
}

type Student struct {
	name  string
	stuNo int
}

//Student类型实现了sayhi()方法
func (s *Student) sayhi() {
	fmt.Printf("Student[%s, %d], say hi.\n", s.name, s.stuNo)
}

type Teacher struct {
	address string
	group   string
}

//Teacher类型实现了sayhi()方法
func (t *Teacher) sayhi() {
	fmt.Printf("Teacher[%s, %s], say hi.\n", t.address, t.group)
}

type MyStr string

func (s *MyStr) sayhi() {
	fmt.Printf("MyStr[%s], say hi.\n", *s)
}

//定义一个函数,接收接口类型的参数,一种接口实现多种行为
//参数接收一个实现了Humaner接口类型中sayhi()方法的变量
func WhoSayHi(i Humaner) {
	i.sayhi()
}

func main() {
	s := &Student{"Go", 1010}
	t := &Teacher{"Google Inc.", "Go"}
	var str MyStr = "hello go" //需要显示写上MyStr类型,没有写,就会默认推导成字符串类型。string类型和MyStr类型,不是同一个类型

	//下面3次调用同一个函数,但实现了不同的表现行为
	WhoSayHi(s) //WhoSayHi的参数是一个普通接口类型,但结构体接收者的类型是*Student,所以s的类型需要是一个指针类型(内存地址)
	WhoSayHi(t)
	WhoSayHi(&str)

	fmt.Println("-----------------------------")

	//创建一个切片,数据类型是Humaner接口类型
	//此例中的变量只有3个,长度千万不要超了,不然第4个开始,就是空指针了,会报错!除非再另外赋值新的变量并加入到切片中
	hs := make([]Humaner, 3)
	hs[0] = s
	hs[1] = t
	hs[2] = &str

	for _, i := range hs {
		//每个i都是实现了Humaner接口类型的结构体变量
		WhoSayHi(i) //两者写法等价
		i.sayhi()   //两者写法等价
	}
}

/*
运行结果:
Student[Go, 1010], say hi.
Teacher[Google Inc., Go], say hi.
MyStr[hello go], say hi.
-----------------------------
Student[Go, 1010], say hi.
Student[Go, 1010], say hi.
Teacher[Google Inc., Go], say hi.
Teacher[Google Inc., Go], say hi.
MyStr[hello go], say hi.
MyStr[hello go], say hi.
*/

简单理解:func WhoSayHi(i Humaner) { i.sayhi() }这段代码中,i Humaner 不需要关心传过来的变量类型是什么,它只看那个变量有没有实现了 sayhi() 方法。只要传过来的变量中实现了 sayhi() 方法,那么Golang就会自行去查找该变量中的函数。如果该变量中没有实现 sayhi() 方法,让 Golang 怎么能找得到?

4.4 接口组合

接口组合的两种表现形式:接口嵌入、接口转换。

4.4.1 接口嵌入

如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式地包含了 interface1 里面的所有方法。

type Humaner interface {
	sayhi()
}

type Songster interface {
	Humaner //引用了Humaner这个接口,同时也包含了Humaner接口中的方法
	sing(lrc string)
}

type Student struct {
	name  string
	stuNo int
}

//Student实现Humaner中的sayhi()方法
func (s *Student) sayhi() {
	fmt.Printf("Student[%s, %d], sayhi.\n", s.name, s.stuNo)
}

//Student同时也实现了sing()这个方法,定义时有参数,实现的时候也必须对应带上参数
func (s *Student) sing(lrc string) {
	fmt.Printf("Student %s, is sing: %s\n", s.name, lrc)
}

func main() {
	var i Songster //声明一个接口类型的变量

	//结构体的接收者是个指针类型,所以需要给它一个内存地址
	s := &Student{"xxxyyy", 999}
	i = s

	i.sayhi() //Songster接口嵌入了Humaner接口,Humaner接口中有sayhi()方法,那么Songster接口也会有这个方法
	i.sing("god is a girl") //Songster接口自己独有的方法
}

/*
运行结果:
Student[xxxyyy, 999], sayhi.
Student xxxyyy, is sing: god is a girl
*/

Songster 接口中嵌入了 Humaner 接口,那么 Songster 就会得到 Humaner 接口中的所有方法。只需要实现 Songster 接口中的方法即可!
还有一个更加简洁的写法,修改上述 main() 中的代码成如下:

func main() {
	//结构体的接收者是个指针类型,所以需要给它一个内存地址
	s := &Student{"xxxyyy", 999}

	s.sayhi() //Songster接口嵌入了Humaner接口,Humaner接口中有sayhi()方法,那么Songster接口也会有这个方法
	s.sing("god is a girl") //Songster接口自己独有的方法
}

运行结果一模一样,大致流程:结构体 Student 已经实现了 sayhi() 和 sing(lrc string) 这两个方法,那么就绑定在了一起。只需要有这个结构体类型的变量,即可找到它已经实现的这两个方法。

4.4.2 接口转换

需要配合接口嵌入来使用。如果 interface2 嵌入了 interface1,那么 interface2 就可以转换给 interface1,因为 interface2 中即包含了它自己本身的方法也包含了 interface1 的方法;而 interface1 不能转换给 interface2,因为 interface1 没有嵌入 interface2,就没有 interface2 中的方法,所以不能转换。

type Humaner interface {
	sayhi()
}

type Songster interface {
	Humaner //引用了Humaner这个接口,同时也包含了Humaner接口中的方法
	sing(lrc string)
}

type Student struct {
	name  string
	stuNo int
}

//Student实现Humaner中的sayhi()方法
func (s *Student) sayhi() {
	fmt.Printf("Student[%s, %d], sayhi.\n", s.name, s.stuNo)
}

//Student同时也实现了sing()这个方法,定义时有参数,实现的时候也必须对应带上参数
func (s *Student) sing(lrc string) {
	fmt.Printf("Student %s, is sing: %s\n", s.name, lrc)
}

func main() {
	var i1 Humaner
	var i2 Songster

	i1 = i2
	fmt.Println("i1=", i1)
}

/*
运行结果:
i1= <nil>
*/

上例中,Songster 接口嵌入了 Humaner 接口,所以 Songster 接口即有自己的方法又有 Humaner 的方法,Songster 可以转换给 Humaner;Humaner 没有 Songster 中的方法,所以不能把 Humaner 给 Songster 。
如果把 Songster 给了 Humaner,就会报错:

func main() {
	var i1 Humaner
	var i2 Songster

	i2 = i1 //报错:Humaner does not implement Songster (missing sing method) ===> Humaner没有实现Songster,缺少sing方法
	fmt.Println("i2=", i2)
}

可以用另外一种更加简洁的概念来理解:超集 子集超集就是方法数量多的那个接口,子集就是方法数量少的那个接口。超集可以给子集子集不能给超集
详看下例代码的注释:

//子集:这个接口的方法数量少
type Humaner interface {
	sayhi()
}

//超集:这个接口的方法数量多
type Songster interface {
	Humaner //引用了Humaner这个接口,同时也包含了Humaner接口中的方法
	sing(lrc string)
}

type Student struct {
	name  string
	stuNo int
}

func (s *Student) sayhi() {
	fmt.Printf("Student[%s, %d], sayhi.\n", s.name, s.stuNo)
}

func (s *Student) sing(lrc string) {
	fmt.Printf("Student %s, is sing: %s\n", s.name, lrc)
}

func main() {
	var i1 Humaner  //子集
	var i2 Songster //超集

	//超集可以给子集
	i1 = i2
	fmt.Println("i1=", i1)
}

/*
运行结果:
i1= <nil>
*/

代码能编得过,没有问题。

4.5 空接口

空接口 interface({}) 不包含任何方法,可以接收任意类型。正因为如此,所有的类型都实现了空接口,空接口可以存储任意类型的数值。空接口就是个万能类型,能够保存任意类型的值

4.5.1 可以给空接口类型赋任意类型的值

示例:

type Any interface{} //any或Any,是空接口一个很好的别名或缩写

type Person struct {
	name string
	age  int
}

func main() {
	var any Any //空接口可以给任何值

	any = 123
	fmt.Printf("any type is: %T, value = %v\n", any, any)

	any = "Golang"
	fmt.Printf("any type is: %T, value = %v\n", any, any)

	any = true
	fmt.Printf("any type is: %T, value = %v\n", any, any)

	any = Person{"Golang", 11}
	fmt.Printf("any type is: %T, value = %v\n", any, any)

	any = &Person{"Rob Pike", 55}
	fmt.Printf("any type is: %T, value = %v\n", any, any)
}

/*
运行结果:
any type is: int, value = 123
any type is: string, value = Golang
any type is: bool, value = true
any type is: main.Person, value = {Golang 11}
any type is: *main.Person, value = &{Rob Pike 55}
*/
4.5.2 Print()系列函数的参数列表就是空接口类型

当函数可以接收任意类型的时候,我们会将其参数类型声明为:空接口 interface{} 类型。最经典的例子就是标准库 fmtPrint 系列的函数。
示例:

func main() {
	var v1 interface{} = 1
	var v2 interface{} = "Google"
	var v3 interface{} = &v2
	var v4 interface{} = struct{ X int }{1}
	var v5 interface{} = &struct{ X int }{1}

	fmt.Printf("v1=%v, v2=%v, v3=%v, v4=%v, v5=%v\n", v1, v2, v3, v4, v5)
}

/*
运行结果:
v1=1, v2=Google, v3=0xc0000341f0, v4={1}, v5=&{1}
*/

标准库 fmtPrintln() 函数的定义:func Println(a ...interface{}) (n int, err error),它的参数就是可以接收 0 个或多个的任意类型参数。

4.5.3 map[string]interface{} 演示

利用 interface{} 可以存放任意类型的值,这个特性。实现 map 多种数据类型的存储、读取。

func main() {
	m := make(map[string]interface{})
	m["int"] = 123
	m["string"] = "hello"
	m["bool"] = true
	m["float64"] = 123.111
	m["nil"] = nil
	m["slice"] = []int{1, 2, 3}

	m2 := make(map[string]int)
	m2["aaa"] = 0

	m["map"] = m2 // 在 map 中,嵌套另一个 map

	for key, value := range m {
		fmt.Printf("key=%s, value=%v\n", key, value)
	}
}

/*
运行结果:
key=string, value=hello
key=bool, value=true
key=float64, value=123.111
key=nil, value=<nil>
key=slice, value=[1 2 3]
key=map, value=map[aaa:0]
key=int, value=123
*/

map 是无序的,所以每次打印结果的顺序都有可能不同。
参考文献:

https://github.com/fengchunjian/goexamples/blob/master/map_interface/null_interface.go
http://blog.ninja911.com/blog-show-blog_id-76.html

4.6 示例:使用 Sorter 接口排序

编写一个接口,实现对不同数据类型的冒泡排序。
要对一组数字或字符串排序,只需要实现三个方法:
1.反映元素个数的 Len() 方法。
2.比较第 i 和 j 个元素的 Less(i, j) 方法。
3.交换第 i 和 j 个元素的 Swap(i, j) 方法。

4.6.1 目录结构

在这里插入图片描述

4.6.2 编写接口的代码

编写一个接口,实现排序时,传过去的实参是这个接口类型。
./sort/sort.go 文件的代码如下:

package sort

//声明接口,并声明需要实现的3个方法
type Sorter interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

//实现冒泡排序的函数
func Sort(data Sorter) { //参数为接口类型
	for pass := 1; pass < data.Len(); pass++ { //data.Len(),获取该类型的长度
		for i := 0; i < data.Len()-pass; i++ {
			//比较两个数值的大小
			if data.Less(i+1, i) {
				data.Swap(i, i+1) //实现交换
			}
		}
	}
}

//检测是否已实现了排序
func IsSorted(data Sorter) bool {
	n := data.Len()
	for i := n - 1; i > 0; i-- {
		if data.Less(i, i-1) {
			return false
		}
	}
	return true
}

//声明IntArray变量为,切片类型存放int
type IntArray []int

//IntArray类型实现接口中的Len()方法,接收者为普通参数
func (p IntArray) Len() int {
	return len(p)
}

//IntArray类型实现接口中的Less(i, j int)bool方法,接收者为普通参数
func (p IntArray) Less(i, j int) bool {
	return p[i] < p[j]
}

//IntArray类型实现接口中的Swap(i, j int)方法,接收者为普通参数
func (p IntArray) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

//定义StringArray变量为,切片类型存放字符串
type StringArray []string

func (p StringArray) Len() int {
	return len(p)
}

func (p StringArray) Less(i, j int) bool {
	return p[i] < p[j]
}

func (p StringArray) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

//对一个int切片进行排序
func SortInts(a []int) {
	Sort(IntArray(a)) //将a转换为IntArray类型
}

//对一个string切片进行排序
func SortStrings(a []string) {
	Sort(StringArray(a)) //将a转换为StringArray类型
}

func IntsAreSorted(a []int) bool {
	return IsSorted(IntArray(a))
}

func StringsAreSorted(a []string) bool {
	return IsSorted(StringArray(a))
}

4.6.3 实现:不同数据类型调用同一个接口,完成排序

./main.go 文件的代码如下:

package main

import (
	"fmt"
	"gitee.com/quanquan616/mySorter/sort"
)

func ints() {
	//初始化一个切片,里面存放int值
	data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
	//将这个切片类型,转换为IntArray类型
	a := sort.IntArray(data)
	//因为IntArray类型已经实现了Sorter接口中的所有方法
	//经过上一步的转换,此时a已经是IntArray类型,所以它与Sorter接口类型吻合
	sort.Sort(a)

	if !sort.IsSorted(a) {
		panic("failed.")
	}

	fmt.Printf("The sorted array is: %v\n", a)
}

func strings() {
	//初始化一个切片,存放string值
	data := []string{"barry", "quanquan616", "Rita", "rita", "Barry", "sally", "Golang", "Sally"}
	//将切片转换成StringArray类型
	a := sort.StringArray(data)
	//StringArray类型也已经实现了Sorter接口的所有方法
	//所以a的类型可以被Sorter接口类型所接收
	sort.Sort(a)

	if !sort.IsSorted(a) {
		panic("failed.")
	}

	fmt.Printf("The sorted array is: %v\n", a)
}

//根据结构体day中的num进行排序
type day struct {
	num       int
	shortName string
	longName  string
}

type dayArray struct {
	data []*day //声明data字段的类型为切片,存放day的指针变量
}

//dayArray类型实现Len()方法,接收者为指针类型
func (p *dayArray) Len() int {
	//p是一个结构体类型,里面有一个data字段,data字段是一个切片类型
	//p.data操作是取出那个一整个切片
	return len(p.data)
}

func (p *dayArray) Less(i, j int) bool { //接收者为指针类型
	//p.data操作得到了一个切片,既然是切片类型就能使用索引进行取值
	return p.data[i].num < p.data[j].num
}

func (p *dayArray) Swap(i, j int) { //接收者为指针类型
	p.data[i], p.data[j] = p.data[j], p.data[i]
}

func days() {
	Sunday := day{0, "SUN", "Sunday"}
	Monday := day{1, "MON", "Monday"}
	Tuesday := day{2, "TUE", "Tuesday"}
	Wednesday := day{3, "WED", "Wednesday"}
	Thursday := day{4, "THU", "Thursday"}
	Friday := day{5, "FRI", "Friday"}
	Saturday := day{6, "SAT", "Saturday"}

	//初始化一个切片,存放day的指针变量
	//结构体dayArray类型中的data字段的类型为切片,切片中存放的数据类型为day的指针变量
	data := []*day{&Tuesday, &Thursday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday}
	a := dayArray{data} //dayArray的数据类型是一个结构体,结构体的语法是大括号"{}"写法
	sort.Sort(&a) //接收者是指针类型,需要一个合法指向。所以必须取地址,把地址的值传过去

	if !sort.IsSorted(&a) {
		panic("days sort failed.")
	}

	for _, d := range data {
		fmt.Printf("%s ", d.longName)
	}
	fmt.Println()
}

func main() {
	ints()
	strings()
	days()
}
4.6.4 最终运行结果
运行结果:
The sorted array is: [-5467984 -784 0 0 42 59 74 238 905 959 7586 7586 9845]
The sorted array is: [Barry Golang Rita Sally barry quanquan616 rita sally]
Sunday Monday Tuesday Wednesday Thursday Friday Saturday 
4.6.5 不将 dayArray 声明为结构体的写法

4.6.3 的代码中,dayArray 是一个结构体类型,里面包含了一个 data 字段,字段类型为一个切片,存放的数据类型是 day 的指针变量。
如果不想使用结构体的话,可以如下写法:(其余代码不改动)

type day struct {
	num       int
	shortName string
	longName  string
}

//类型为一个切片,存放的数据类型为day的指针变量
type dayArray []*day

func (p *dayArray) Len() int {
	//dayArray是一个切片了
	return len(*p) //接收者是指针类型,因为指针类型存放的是内存地址的值,必须先取出值,才能进行计算
}

func (p *dayArray) Less(i, j int) bool {
	//接收者是指针类型,需要先用星花*运算符取出地址中的值,才能有后续的操作
	return (*p)[i].num < (*p)[j].num //(*p)操作是取出内存地址中的那个一个整个切片
}

func (p *dayArray) Swap(i, j int) {
	(*p)[i], (*p)[j] = (*p)[j], (*p)[i]
}

func days() {
	Sunday := day{0, "SUN", "Sunday"}
	Monday := day{1, "MON", "Monday"}
	Tuesday := day{2, "TUE", "Tuesday"}
	Wednesday := day{3, "WED", "Wednesday"}
	Thursday := day{4, "THU", "Thursday"}
	Friday := day{5, "FRI", "Friday"}
	Saturday := day{6, "SAT", "Saturday"}

	data := []*day{&Tuesday, &Thursday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday}
	//dayArray是一个存放day指针变量的切片了
	a := dayArray(data)
	//接收者是指针类型,所以必须传址
	sort.Sort(&a)

	if !sort.IsSorted(&a) {
		panic("days sort failed.")
	}

	for _, d := range data {
		fmt.Printf("%s ", d.longName)
	}
	fmt.Println()
}

func main() {
	days()
}

/*
运行结果:
Sunday Monday Tuesday Wednesday Thursday Friday Saturday 
*/

可以看到,运行结果一模一样。
4.6.3 代码中的写法是将切片再次封装成为一个数据类型,通过操作该数据类型中的字段,得到一整个切片,然后再根据下标进行取值。
本例中的写法是声明一个变量,类型为存放指针变量的切片,每次都是直接操作这个变量。

4.6.6 参考文献

本节内容均来源于:
原文作者:Go 技术论坛文档:《Go 入门指南()》
转自链接:https://learnku.com/docs/the-way-to-go/the-first-example-of-117-sorting-using-the-sorter-interface/3653
版权声明:翻译文档著作权归译者和 LearnKu 社区所有。转载请保留原文链接

4.7 反射包reflect

反射可以在运行时检查类型和变量,例如它的大小、方法。
变量的最基本信息是:类型和值。反射包的 Type 用来表示一个类型,反射包的 Value 为值提供了反射接口。

4.7.1 最基本的两个函数

reflect.TypeOf(),返回变量的类型。
reflect.ValueOf(),返回变量的值。
示例:



五、类型断言

类型查询也叫类型断言,常用的有两种方式:
1.comma-ok 断言(if
2.type-switch 测试(switch

5.1 if 实现类型断言

被判断的变量必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)

5.1.1 基本语法

if value, ok := varName.(Type); ok == true {...}
varName.(Type) 采用变量名.(数据类型)的方式获取到:该变量的值和该变量是否为指定数据类型的 bool 值。

5.1.2 最基本的使用
if v, ok := varI.(T); ok {
    Process(v)
    return
}

如果断言成功(varI 是 T 类型),v 是 varI 本身的值,ok 会是布尔值 true。否则 v 是 T 类型的零值,ok 会是布尔值 false,不会造成运行时错误。
注意:varI 需要是一个接口变量。

5.1.3 简洁写法

多数情况下,可能只是想在 if 中测试一下 ok 的值,此时使用下面的写法会显得更加简洁、方便。
示例:

if _, ok := varI.(T); ok {
    //...
}
5.1.4 示例
type Student struct {
	name  string
	stuNo int
}

func main() {
	//声明一个长度为3的切片,里面的元素都是空接口。空接口可以接收任意的数据类型
	items := make([]interface{}, 3)

	items[0] = 111
	items[1] = "Golang"
	items[2] = Student{"xxx", 999}

	//遍历,第一个返回元素的下标,第二个返回数据值。这里就是返回接口中的值
	for index, data := range items {

		//data.(int)返回两个值,第一个是该变量本身的值,第二个返回该变量是否为指定数据类型的bool
		if value, ok := data.(int); ok == true {
			fmt.Printf("items[%d] type is int, value=%d\n", index, value)
		} else if value, ok := data.(string); ok == true {
			fmt.Printf("items[%d] type is string, value=%s\n", index, value)
		} else if value, ok := data.(Student); ok == true { //data这个变量是否为Student类型,Student是自定义的结构体类型
			fmt.Printf("items[%d] type is Student, value=%+v\n", index, value)
		}
	}
}

/*
运行结果:
items[0] type is int, value=111
items[1] type is string, value=Golang
items[2] type is Student, value={name:xxx stuNo:999}
*/

5.2 type-switch

在程序运行时的时候进行类型分析。在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。

5.2.1 基本写法
switch t := data.(type) {
case type:
	...
case type:
	...
}

注意:
1.switch t := data.(type) 这条语句中,type 是关键字的那个type,不要写明数据类型。变量 t 在内存中占据两个字长:一个是其本身的类型,还有一个是其本身的值。
2.case type 语句中的 type 是写明具体的一个数据类型(例如:int, string, StructName),case type 根据 switch 语句中的 t 会去自行匹配。
3.data.(type) 必须在 switch 中使用,否则就报错

5.2.2 简洁写法

如果仅仅只是测试变量的类型,不想用它的值,那么就可以不需要赋值语句。
示例:

switch areaIntf.(type) {
case *Square:
    //...
case *Circle:
    //...
default:
    //...
}
5.2.3 示例1:基本示例

将所有值放入一个切片中,切片的数据类型是空接口。(空接口是万能类型,允许接收任意类型)
示例:

type Student struct {
	name  string
	stuNo int
}

func main() {
	//声明一个长度为4的切片,里面的元素都是空接口。空接口可以接收任意的数据类型
	items := make([]interface{}, 4)

	items[0] = 111
	items[1] = "Golang"
	items[2] = Student{"xxx", 999}
	items[3] = false

	//遍历,第一个返回元素的下标,第二个返回数据值。这里就是返回接口中的值
	for index, data := range items {
		switch data.(type) { //type是关键字,会自行跟下面各个case去匹配
		case int:
			fmt.Printf("items[%d] type is int, value=%d\n", index, data)
		case string:
			fmt.Printf("items[%d] type is string, value=%s\n", index, data)
		case Student:
			fmt.Printf("items[%d] type is Student, value=%+v\n", index, data)
		case bool:
			fmt.Printf("items[%d] type is bool, value=%v\n", index, data)
		}
	}
}

/*
运行结果:
items[0] type is int, value=111
items[1] type is string, value=Golang
items[2] type is Student, value={name:xxx stuNo:999}
items[3] type is bool, value=false
*/
5.2.4 示例2:判定每个值的类型

给定一些值,根据每个值的实际类型执行不同的动作。
示例:

func classifier(params ...interface{}) {
	for _, item := range params {
		switch t := item.(type) {
		case int, int16, int32, int64:
			fmt.Printf("%v is an int type.\n", t)
		case bool:
			fmt.Printf("%v is a bool type.\n", t)
		case float32, float64:
			fmt.Printf("%v is a float.\n", t)
		case string:
			fmt.Printf("%v is a string.\n", t)
		case nil:
			fmt.Printf("it's a nil.\n")
		default:
			fmt.Println("%v is unknow.\n", t)
		}
	}
}

func main() {
	classifier(13, -14.3, "BELGIUM", complex(1, 2), nil, false)
}

/*
运行结果:
13 is an int type.
-14.3 is a float.
BELGIUM is a string.
%v is unknow.
 (1+2i)
it's a nil.
false is a bool type.
*/
5.2.5 示例3:type-switch 配合匿名函数

示例:

type specialString string //声明一个自定义类型,其底层类型为string

var whatItThis specialString = "Hello Golang." //自定义类型的底层类型为string,因此可以把字符串复制给它

func TypeSwitch() {
	testFunc := func(any interface{}) { //匿名函数的参数是空接口(万能接口),可以接收任何值
		switch v := any.(type) { //v包含两个字长:一个是类型,另一个是本身的值
		case bool:
			fmt.Printf("v type is bool, value = %v\n", v)
		case int:
			fmt.Printf("v type is int, value = %v\n", v)
		case float32, float64:
			fmt.Printf("v type is float, value = %v\n", v)
		case string:
			fmt.Printf("v type is string, value = %v\n", v)
		case specialString:
			fmt.Printf("v type is a customize type: specialString, value = %v\n", v)
		default:
			fmt.Println("v type is unkonw")
		}
	}
	testFunc(whatItThis)
}

func main() {
	TypeSwitch()
}

/*
运行结果:
v type is a customize type: specialString, value = Hello Golang.
*/

本例的参考文献:

https://learnku.com/docs/the-way-to-go/119-empty-interface/3655

5.2.6 注意事项
5.2.6.1 case 语句中列举的类型,都必须实现对应的接口

所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口。如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。

5.2.6.2 在 type-switch 不允许有 fallthrough

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值