笔记声明
本人智商较低,好记性不如烂笔头。笔记记录了网上一套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{}
类型。最经典的例子就是标准库 fmt
中 Print
系列的函数。
示例:
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}
*/
标准库 fmt
中 Println()
函数的定义: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。