Go语言核心编程第3章“类型系统”

学习目标:

Go语言核心编程第3章 “类型系统”

学习内容:

Go语言核心编程 第3章 “类型系统”

第3章 类型系统

3.1 类型简介

3.1.1 命名类型和未命名类型

命名类型:类型都可以通过标识符表示
未命名类型:类型由预声明类型,关键字和操作符组合而成
命名类型包括:
1.Go语言20个预声明简单类型都是命名类型
bool、int、int8、int16、int32、int64、float32、float64、uint、uint8、uint16、uint32、uint64、uintptr、complex32、complex64、byte、rune、string、error
2.用户自定义类型
未命名类型(类型字面量)包括:
1.数组array
2.切片slice
3.字典map
4.通道channel
5.指针pointer
6.函数字面量function
7.结构struct (未使用type)
8.接口interface (未使用type)

package main
import "fmt"
type Person struct { //命名类型
	name string
	age  int
} 
func main {
	a := struct { //未命名类型
		name string
		age  int
	}{"aa",12}
}
3.1.2 底层类型

所有"类型"都有一个underlying type(底层类型)
底层数据类型规则
1.预声明类型和类型字面量底层类型是自身。
2.自定义类型的底层数据类型是递归向下查找的,直到查到的类型是预声明类型或类型字面量。

// T1和T2的底层类型是string
type T1 string
type T2 T1
// T3和T4的底层类型是[]string
type T3 []string
type T4 T3
// T5和T6的底层类型是[]T1,因为[]T1是数组,数组是类型字面量
type T5 []T1
type T6 T5
特别注意这里的T6、T5与T3、T4的底层类型是不一样的,一个是[]T1,另一个是[]string
3.1.3 类型相同和类型赋值
3.1.3.1 类型相同

Go是强类型的语言,编译器在编译时会进行严格的类型校验。
类型相同的判断条件:
1.两个命名类型相同的条件是两个类型声明的语句完全相同。
2.命名类型和未命名类型永远不相同。
3.两个未命名类型相同的条件是它们的类型声明字面量的结构相同,并且内部元素的类型相同。
4.通过类型别名语句声明的两个类型相同。
Go1.9引入类型别名语法type T1 = T2,T1的类型完全和T2一样。
引入别名主要有如下原因:
1.为了解决新旧包的迁移兼容问题,比如context包先前并不在标准库里面,后面迁移到了标准库。
2.Go的按包进行隔离的机制不太精细,有时我们需要将大包划分为几个小包进行开发,但需要在大包里面暴露全部的类型给使用者。
3.解决新旧类型的迁移问题,新类型先是旧类型的别名,后续的软件都基于新类型编程,在合适的时间将新类型升级为和旧类型不兼容,常用于软件的柔性升级。

3.1.3.2 类型赋值

不同类型的变量之间一般是不能直接相互赋值的,除非满足一定的条件。
类型为T1的变量a可以赋值给类型为T2的变量b,称类型T1可以赋值给类型T2。

var b T2 = a 

a可以赋值给变量b必须要满足如下条件中的一个:
1.T1和T2的类型相同
2.T1和T2具有相同的底层类型,并且T1和T2里面至少有一个是未命名类型

package main
import "fmt"
func main() {
	type x string
	var s string
	var q x
	q = s 运行报错
	fmt.Println(q)
}
cannot use s (type string) as type x in assignment
-------------------------
package main
import "fmt"
func main() {
	type x []string
	var s []string
	var q x
	q = s
	fmt.Println(q)
}
运行成功 []

3.T2是接口类型,T1是具体类型,T1的方法集是T2方法集的超集。
4.T1和T2都是通道类型,它们拥有相同的元素类型,并且T1和T2中至少有一个是未命名类型。
5.a是预声明标识符nil,T2是pointer、funcition、slice、map、channel、interface类型中的一个。
6.a是一个字面常量值,可以用来表示类型T的值

package main
import "fmt"
type Map map[string]string
type iMap Map
func main() {
	mp := make(map[string]string, 10)
	mp["hi"] = "xx"
	//mp与ma有相同的底层类型map[string]string,并且是未命名类型,所以mp可以直接赋值给ma
	var ma Map = mp
	//var im iMap = ma
	//im与ma虽然具有相同的底层类型map[string]string,但它们中没有一个是未命名类型
	//所以不能赋值,如下语句不能通过编译
	
	ma.Print()
	//Map实现了Print(),所以其可以赋值给接口类型变量
	var i interface {
		Print()
	} = ma
	i.Print()

	s1 := []int{1, 2, 3}
	var s2 slice
	s2 = s1 //底层类型相同,且其中一个是非命名类型
	s2.Print()
}
type slice []int
//只要底层类型是slice、map等支持range的类型字面量,新类型任然可以使用range迭代
func (m Map) Print() {
	for _, key := range m {
		fmt.Println(key)
	}
}
func (m iMap) Print() {
	for _, key := range m {
		fmt.Println(key)
	}
}
func (s slice) Print() {
	for _, v := range s {
		fmt.Println(v)
	}
}
3.1.4 类型强制转换

Go语言中如果不满足自动转换的条件,则必须进行强制类型转换
强制类型的语法格式:

var a T = (T) (b)

强制类型转换规则
非常量类型的变量 x 可以强制转化并传递给类型 T ,需要满足如下任一条件:
(1) x可以直接赋值给T类型变量
(2) x和T具有相同的底层类型

package main
import "fmt"
func main() {
	type x string
	var s string
	var q x
	q = (x)(s)
}

(3) x的类型和T都是未命名的指针类型,并且指针指向的类型具有相同的底层类型
(4) x的类型和T都是整型或者都是浮点型

package main
import "fmt"
func main() {
	var s int
	var q uint16
	q = (uint16)(s)
	fmt.Println(q)
}

(5) x的类型和T都是复数类型

package main
import "fmt"
func main() {
	var s complex64
	var q complex128
	q = (complex128)(s)
	fmt.Println(q)
}

(6) x是整数值或[]byte类型的值,T是string类型
(7) x是一个字符串,T是[]byte或[]rune
字符串和字节切片之间的转换最常见,示例如下:

func main() {
	s := "hello,你好"
	var a []byte
	a = []byte(s)

	var b string
	b = string(a)

	var c []rune
	c = []rune(s)

	fmt.Printf("%T\n", a) // []uint8 	byte 是 int8 的别名
	fmt.Printf("%T\n", b) // string
	fmt.Printf("%T\n", c) // []int32 	rune 是 int32 的别名
}

注意:
数值类型和string类型之间的相互转换可能造成值部分丢失;
其他的转换仅是类型的转换,不会造成值的改变。
string和数字之间的转换可使用标准库strconv

3.2 类型方法

3.2.1 自定义类型

用户自定义类型使用关键字type

type newtype oldtype
oldtype可以是自定义类型,未命名类型,预声明类型中的任意一种
1.newtype和oldtype具有一样的底层类型。
2.newtype和oldtype都继承了底层类型的操作集合(比如 map支持range迭代访问,新类型也支持)3.newtype不会继承oldtype的方法。
4.无论oldtype什么类型,使用type声明的newtype都是命名类型。
type INT int //INT是一个使用预声明类型声明的自定义类型
type Map map[string]string //INT是一个使用类型字面量声明的自定义类型
type mymap Map //INT是一个使用自定义类型Map声明的自定义类型
3.2.1.1 struct类型

struct类型是Go语言自定义类型的普遍形式,是 Go 语言类型扩展的基石,也是 Go 语言面向对象承载的基础。如果使用type语句声明struct作为新类型,则这个新类型就是命名类型,反之struct划为未命名类型。

使用 type 自定义的结构类型属于命名类型
type XXXName struct {
    field1 type1
    field2 type2
}
errorString 是一个自定义结构类型,也是命名类型
type errorString struct {
    s string
}
结构字面量属于未命名类型
struct {
    field1 type1
    field2 type2
}
struct{} 是未命名类型空结构
var s = struct{}{}
-------------------------------------------
struct初始化方法
type Person struct {
    name string
    age int
}
第一种方法:按照字段顺序进行初始化
a := Person{"Tom", 25}
b := Person{
    	"Tom",
          25}
c := Person{
    "Tom",
      25,	// 必须加逗号
      }
不推荐这种写法,一旦结构体增加字段,则需要重新修改初始化语句的顺序。
第一种方法:指定字段名进行初始化
推荐这种写法,结构体增加字段后,无需修改原有初始化语句的顺序。
a := Person{name:"Tom", age:25}
b := Person{
    name:"Tom", 
    age:25}
b := Person{
    name:"Tom", 
    age:25, // 加逗号
}	
注意:初始化语句结尾的 } 独占一行,则最后一个字段的后面一定要带上逗号      
第三种方法:使用new创建内置函数,字段默认初始化为其类型的零值,返回值是指向结构的指针
p := new(Person)
// 此时 name 为 "", age 为 0
这种方法不常用,一般使用struct都不会将所有字段初始化为零值
第四种方法:一次初始化一个字段
p := Person{}
p.name = "Tom"
p.age = 25
这种方法不常用,这是一种结构化的编程思维,没有封装,违背了struct本身抽象封装的理念
第五种方法:使用构造函数进行初始化
这是推荐的一种方法,当结构发生变化时,构造函数可以屏蔽细节
case 1
```go
package main
import "fmt"
type errorString struct {
	s string
}
func New(text string) error {
	return &errorString{text}
}
func (e *errorString) Error() string {
	return e.s
}
func main() {
	err := New("this is a error")
	if err != nil {
		fmt.Println(err.Error())
	}
}
case 2
type Cat struct {
	Color string
	Name  string
}
func NewCatByName(name string) *Cat {
	return &Cat{
		Name: name, // 忽略 Color 字段
	}
}
func NewCatByColor(color string) *Cat {
	return &Cat{
		Color: color, // 忽略 Name 字段
	}
}
-------------------------------------------
结构字段的特点
结构的字段可以是任意的类型。
基本类型、接口类型、指针类型、函数类型都可以作为struct的字段。
结构字段的类型名必须唯一, struct字段类型可以是普通类型,也可以是指针。
另外,结构支持内嵌自身的指针,这也是实现树形和链表等复杂数据结构的基础。
type Element struct {
	next,prev *Element
	list *List
	Value interface{}
}
--------------------------------------------------------
匿名字段
结构体允许其成员字段在声明时没有宇段名而只有类型,这种形式的字段被称为类型内嵌或匿名字段。
被匿名嵌入的字段必须是命名类型或命名类型的指针,类型字面量不能作为匿名字段使用。
类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而己。
结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。
一个结构体里面不能同时存在某一类型及其指针类型的匿名字段,原因是二者的字段名相等。
如果嵌入的字段来自其他包,则需要加上包名,并且必须是其他包可以导出的类型。
type File {
	*file
}
3.2.1.2 自定义接口类型
// interface{} 是接口字面量类型标识, 所以i是非命名类型交量
var i interface{}
// Reader 是自定义接口类型,属于命名类型
type Reader interface {
    Read (p []byte) (n int , err error )
}
3.2.2 方法

Go语言的类型方法是一种对类型行为的封装,本质上就是一个函数,没有使用隐形的指针。
关键字func和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。
如果一个函数有接收者,这个函数就被称为方法。

类型方法接收者是值类型
func (t TypeName)MethodName(ParamList) (Returnlist)  {
	// method body
}
类型方法接收者是指针
func (t *TypeName)MethodName(ParamList) (Returnlist)  {
	//method body
}
---------------------------------------------------------------
将类型的方法改写成常规的函数
func TypName_MethodName(t TypeName, otherParamList) (Returnlist)  {
	//method body
}
func TypName_MethodName(t *TypeName, otherParamList) (Returnlist) {
	//method body
}
t是接收者,可以自由指定名称
TypeName为命名类型的类型名
MethodName为方法名,是一个自定义标识符
ParamList是形参列表
ReturnList是返回值列表
-----------------------------------------------------------------
具体示例
type SliceInt []int
func (s SliceInt) Sum int {
	sum := 0
	for _, i := range s {
		sum += i
	}
}
var s SliceInt = []int{1, 2, 3 ,4}
s.sum()

类型方法的特点:
1.可以为命名类型增加方法(除了接口),非命名不能自定义方法。
2.为类型增加方法有一个限制,方法的定义必须和类型的定义同一个包中
3.方法的命名空间的可见性和变量一样,大写开头的方法可以在包外被访问,否则只能在包内可见。
4.使用type定义的自定义类型是一个新类型,新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承。

package main
import "fmt"
type Map map[string]string
func (m Map) Print() {
	//底层类型支持range运算,新类型可用
	for _, key := range m {
		fmt.Println(key)
	}
}
type MyInt int
func main() {
	var a MyInt = 10
	var b MyInt = 10
	// int类型支持的加减乘除运算,新类型同样可用
	c := a + b
	d := a * b
	fmt.Printf("%d\n", c)
	fmt.Printf("%d\n", d)
}

3.3 方法调用

类型方法本质就是函数,只是一种特殊的语法书写。

3.3.1 一般调用

类型方法的一般调用方式

TypeInstancceName.MethodName(ParamList)
TypeInstanceName: 类型实例名或指向实例的指针变量名
MethodName:类型方法名
ParamList:方法实参
------------------------------------------------
package main
type T struct {
	a int
}
func (t T) Get() int {
	return t.a
}
func (t *T) Set(i int) {
	t.a = i
}
func main() {
	var t = &T{}
	t.Set(2)
	t.Get()
}
------------------------------------
调用类型方法
值类型既可以调用值接收者的方法,也可以调用指针接收者的方法。
指针类型可以调用指针接收者的方法,也可以调用值接收者的方法。
package main
import "fmt"
type Person struct {
	name string
}
func (p *Person) ChangeName(newName string) {
	p.name = newName
}
func (p Person) Change(newName string) {
	p.name = newName
}
func main() {
	p := Person{"John"}
	p.ChangeName("Jane")
	fmt.Println(p.name)
	r := &Person{"John"}
	r.ChangeName("Jane")
	fmt.Println(r.name)
	s := Person{"John"}
	s.Change("Jane")
	fmt.Println(s.name)
	q := &Person{"John"}
	q.Change("Jane")
	fmt.Println(q.name)
}
Jane
Jane
John
John
对于值接收者
	1.值类型调用者
	  方法会使用调用者的一个副本,类似于"传值"使用值的引用来调用方法。
	2.指针类型调用者	指针被解引用为值
	  q.Change() 实际上是 (*q).Change() 
对于指针接收者
	1.值类型调用者
	  p.ChangeName()实际上是(&p).ChangeName() 
	2.指针类型调用者
	  实际上是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝一份指针。

注意
实现接收者是值类型的方法,自动实现了接收者是指针类型的方法。
实现接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

可运行
package main
type isimp interface {
	ChangeName(newName string)
	Change(newName string)
}
type Person struct {
	name string
}
func (p *Person) ChangeName(newName string) {
	p.name = newName
}
func (p Person) Change(newName string) {
	p.name = newName
}
func main() {
	var isImp isimp = &Person{"John"}
	isImp.ChangeName("Jane")
	isImp.Change("Joy")
}
-------------------------------------------
报错
package main
type isimp interface {
	ChangeName(newName string)
	Change(newName string)
}
type Person struct {
	name string
}
func (p *Person) ChangeName(newName string) {
	p.name = newName
}
func (p Person) Change(newName string) {
	p.name = newName
}
func main() {
	var isImp isimp = Person{"John"}
	isImp.ChangeName("Jane")
	isImp.Change("Joy")
}
3.3.2 方法值

变量x的静态类型是T,M是类型T的一个方法,x.M被称为方法值
x.M是一个函数类型变量,可以赋值给其他变量,并像普通的函数名一样使用

f := x.M
f(args...)
等价于
x.M(args ...)

方法值是一个带有闭包的函数变量,接收值隐式地绑定到方法值的闭包环境中。
后续调用不需要再显显式地传递接收者。

package main
import "fmt"
type T struct {
	a int
}
func (t T) Get() int {
	return t.a
}
func (t *T) Set(i int) {
	t.a = i
}
func (t *T) Print() {
	fmt.Printf("%p,%v,%d \n", t, t, t.a)
}
func main() {
	var t = &T{}
	f := t.Set
	f(2)
	t.Print()
	f(3)
	t.Print()
}
0xc0000160b8,&{2},2
0xc0000160b8,&{3},3
3.3.3 方法表达式

方法表达式提供一种语法将类型方法调用显式地转换为函数调用,接收者必须显式地传递进去。

package main
import "fmt"
type T struct {
	a int
}
func (t T) Get() int {
	return t.a
}
func (t *T) Set(i int) int {
	t.a = i
	return t.a
}
func (t *T) Print() {
	fmt.Printf("%p, %v, %d\n", t, t, t.a)
}
表达式T.Get和(*T).Set被称为方法表达式(method expression)
方法表达式可以看作函数名,只不过这个函数的首个参数是接收者的实例或指针
T.Get的函数签名是func (t T) int(*T).Set 的函数签名是func(t *T, i int)
----------------------------------------------------------------------
注意T.Get不能写成(*T).Get,(*T).Set也不能写成T.Set 
在方法表达式中编译器不会做自动转换
package main
import "fmt"
type T struct {
	a int
}
func (t T) Get() int {
	return t.a
}
func (t *T) Set(i int) int {
	t.a = i
	return t.a
}
func (t *T) Print() {
	fmt.Printf("%p, %v, %d\n", t, t, t.a)
}
func main() {
	var t T
	set := (T).Set(t, 1) //报错
	set := (T).Set(&t, 1) //报错
	set := (*T).Set(t, 1) //报错
	set := (*T).Set(&t, 1) //不报错
	fmt.Println(set)
}
3.3.4 方法集

命名类型的方法接收者有两者类型
1.值类型
2.指针类型
无论接收者什么类型,方法和函数的实参传递都是值拷贝。
如果接收者是值类型,则传递的是值的副本。
如果接收者是指针类型,则传递的是传递的是指针的副本。

package main 
import "fmt"
type Int int
func (a Int) Max(b Int) Int {
	if a >= b {
		return a
	} else {
		return b
	}
}
func (i *Int) Set(a Int) {
	*i = a
}
func (i Int) Print() {
	fmt.Printf("value=%d\n",i)
}
func main(){
	var a Int = 10
	var b Int = 20
	c := a.Max(b)
	c.Print() //value=20
	(&c).Print() //内部转换为c.Print()
	
	a.Set(20) //内部转换为(&a).Set(20)
	a.Print() //value=20

	(&a).Set(30)
    a.Print()//value=30
}
3.3.5 值调用和表达式调用的方法集

前面提到过:具体实例类型直接调用其方法时,编译器会根据调用方法进行自动转换。
下列给出两种情况,编译器不会进行自动转换
1.通过类型字面量显式地进行值调用和表达式调用

type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
这种字面量显式地进行调用,无论值调用和表达式调用,编译器都不会进行自动转换,会参数校验
*Data 方法集 TestValue(){} TestPointer(){}
(*Data)(&Data{}).TestValue() //值调用
(*Data)(&Data{}).TestPointer() //值调用
(Data)(Data{}).TestValue()
如下调用失败
(Data)(Data{}).TestPointer() //值调用
Data.TestPointer(Data{}) //表达式调用

2.通过类型变量进行值调用和表达式调用
使用值调用会自动转换
使用表达式不会自动转换

使用表达式不会自动转换
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
	var a Data = Data{}
	//表达式调用
	Data.TestValue(a)
	Data.TestPointer(a) //报错needs pointer receiver: (*Data).TestPointer
	(*Data).TestPointer(&a)
	(*Data).TestValue(&a)
	(*Data).TestValue(a) //报错Cannot use 'a' (type Data) as the type *Data
}
使用值调用会自动转换
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
	var a Data = Data{}
	//值调用
	a.TestValue()
	a.TestPointer()
	(&a).TestPointer()
	(&a).TestPointer()
}

3.4 组合和方法集

struct为Go提供了强大的类型扩展,主要体现在两个方面
1.struct 可以嵌入任意其他类型的字段
2.struct 可以嵌套自身的指针类型的字段
Go语言中的数据就是数据,逻辑就是逻辑,二者是“正交”的,底层实现上没有相关性。

3.4.1 组合

使用type定义的新类型不会继承原有类型的方法,有个特例就是命名结构类型
命名结构类型可以嵌套其他的命名类型的字段,外层的结构类型是可以调用嵌入字段类型的方法,这种调用既可以是显式的调用,也可以是隐式的调用
这就是 Go 的“继承”,准确地说这就是 Go 的“组合”。
Go语言没有继承的语义,结构和字段之间是“has a”的关系,而不是“is a”的关系,没有父子的概念,仅仅是整体和局部的概念,所以后续统称这种嵌套的结构和字段的关系为组合
struct 中的组合非常灵活,可以表现为水平的字段扩展,**由于struct可以嵌套其他struct字段,所以组合也可以分层次扩展。struct 类型中的字段称为“内嵌字段”。

3.4.1.1 内嵌字段的初始化和访问

内嵌字段的访问和方法调用遵照的规约如下:
struct 的字段访问使用点操作符“.”
struct 的字段可以嵌套很多层,只要内嵌的字段是唯一的即可,不需要使用全路径进行访问。

package main
type X struct {
	a int
}
type Y struct {
	X
	b int
}

type Z struct {
	Y
	c int
}

func main() {
	x := X{a: 1}
	y := Y{
		X: x,
		b: 2,
	}
	z := Z{
		Y: y,
		c: 3,
	}
    //z.a, z.Y.a, z.Y.X.a 三者是等价的, z.a z.Y.a 是 z.Y.X.a 的简写
	println(z.a, z.Y.a, z.Y.X.a) //1 1 1
	z = Z{}
	z.a = 2
	println(z.a, z.Y.a, z.Y.X.a) //2 2 2
}

在 struct 的多层嵌套中,不同嵌套层次可以有相同的字段,此时最好使用完全路径进行访问和初始化。在实际数据结构的定义中应该尽量避开相同的字段,以免在使用中出现歧义。

package main
type X struct {
	a int
}
type Y struct {
	X
	a int
}
type Z struct {
	Y
	a int
}
func main() {
	x := X{a: 1}
	y := Y{
		X: x,
		a: 2,
	}
	z := Z{
		Y: y,
		a: 3,
	}
	//此时的z.a, z.Y.a, z.Y.X.a 代表不同的字段
	println(z.a, z.Y.a, z.Y.X.a) // 3 2 1
	z = Z{}
	z.a = 4
	z.Y.a = 5
	z.Y.X.a = 6
	//此时的z.a, z.Y.a, z.Y.X.a 代表不同的字段
	println(z.a, z.Y.a, z.Y.X.a) // 4 5 6
}
3.4.1.2 内嵌字段的方法调用

struct 类型方法调用也使用点操作符不同嵌套层次的字段可以有相同的方法,外层变量调用内嵌字段的方法时也可以像嵌套字段的访问一样使用简化模式。如果外层字段和内层字段有相同的方法,则使用简化模式访问外层的方法会覆盖内层的方法。
即在简写模式下,Go 编译器优先从外向内逐层查找方法,同名方法中外层的方法能够覆盖内层的方法。

package main
import "fmt"
type X struct {
	a int
}
type Y struct {
	X
	b int
}
type Z struct {
	Y
	c int
}
func (x X) Print() {
	fmt.Printf("In X, a = %d\n", x.a)
}
func (x X) XPrint() {
	fmt.Printf("In X, a = %d\n", x.a)
}
func (y Y) Print() {
	fmt.Printf("In Y, b = %d\n", y.b)
}
func (z Z) Print() {
	fmt.Printf("In Z, c = %d\n", z.c)
	//显式的完全路径调用内嵌字段的方法
	z.Y.Print()
	z.Y.X.Print()
}

func main() {
	x := X{a: 1}
	y := Y{
		X: x,
		b: 2,
	}
	z := Z{
		Y: y,
		c: 3,
	}
	//从外向内查找,首先找到的是 Z 的 Print() 方法
	z.Print()
	//从外向内查找,最后找到的是 x 的 XPrint()方法
	z.XPrint()
	z.Y.XPrint()
}
In Z, c = 3
In Y, b = 2
In X, a = 1
In X, a = 1
In X, a = 1

不推荐在多层的 struct 类型中内嵌多个同名的字段;但是并不反对 struct 定义和内嵌字段同名方法的用法,因为这提供了一种编程技术,使得struct能够重写内嵌字段的方法,提供面向对象编程中子类覆盖父类的同名方法的功能。

3.4.2 组合的方法集

组合结构的方法集有如下规则:
1.若类型S包含匿名字段T,则S的方法集包含T的方法集。
2.若类型S包含匿名字段T,则S的方法集包含T和T方法集。
3.不管类型S中嵌入的匿名字段是T还是*T,S 方法集总是包含T和T方法集。
Go编译器会对方法调用进行自动转换,为了阻止自动转换,使用方法表达式的调用方式,进行演示方法集的规约。

package main
type X struct {
	a int
}
type Y struct {
	X
}
type Z struct {
	*X
}
func (x X) Get() int {
	return x.a
}
func (x *X) Set(i int) {
	x.a = i
}
func main() {
	x := X{a: 1}
	y := Y{
		X: x,
	}
	println(y.Get()) // 1
	此处编译器做了自动转换
	y.Set(2)
	println(y.Get()) // 2
	为了不让编译器做自动转换,使用方法表达式调用方式
	Y内嵌字段X,所以type y的方法集是Get,type *Y的方法集是Set Get
	(*Y).Set(&y, 3)
	Y.Set(y, 3) 编译不能通过
	println(y.Get()) 3
	z := Z{
		X: &x,
	}
	按照嵌套字段的方法集的规则
	Z内嵌字段*X,所以 type Z和type *Z方法集都包含类型X定义的方法Get和Set
	为了不让编译器做自动转换,仍然使用方法表达式调用方式
	Z.Set(z, 4)
	println(z.Get()) // 4
	(*Z).Set(&z, 5)
	println(z.Get()) // 5
}

到目前为止还没有发现方法集有多大的用途,而且通过实践发现,Go 编译器会进行自动转换,看起来不需要太关注方法集,这种认识是错误的。编译器的自动转换仅适用于直接通过类型实例调用方法时才有效类型实例传递给接口时,编译器不会进行自动转换,而是会进行严格的方法集校验。
Go函数的调用实参都是值拷贝,方法调用参数传递也是一样的机制,具体类型变量传递给接口时也是值拷贝,如果传递给接口变量的是值类型,但调用方法的接收者是指针类型,则程序运行时虽然能够将接收者转换为指针,但这个指针是副本的指针,并不是我们期望的原变量的指针。
所以语言设计者为了杜绝这种非期望的行为,在编译时做了严格的方法集合的检查,不允许产生这种调用如果传递给接口的变量是指针类型,则接口调用的是值类型的方法,程序运行时能够自动转换为值类型,这种转换不会带来副作用,符合调用者的预期,所以这种转换是允许的,而且这种情况符合方法集的规约。

3.5 函数类型

函数类型分为两种
1.函数命名类型
2.函数字面量类型(未命名类型)

3.5.1 函数字面量类型

func(InputTypeList) OutputTypeList

有名函数 func FunctionName()
匿名函数 func()
均属于函数字面量类型

3.5.2 函数命名类型

可以使用type NewFuncType FuncLiteral语法定义一种新类型:函数命名类型
NewFuncType为新定义的函数命名类型
FuncLiteral为函数字面量类型
FuncLiteral为NewFuncType底层类型

3.5.3 函数签名

函数签名是函数的字面量类型
有名函数和匿名函数的函数签名可以相同
Go中可以嵌入多个结构体,但是多个结构体不能有相同的方法,如果有参数和方法名完全相同的方法,在编译的时候就会报错。

package main
import "fmt"
func main() {
   man := Man{}
   fmt.Println(man)
   //下面的代码编译会报错
   //man.doEat()
}
type Man struct {
   FatherA
   FatherB
}
func (p FatherA) doEat() {
   fmt.Printf("FatherA eat")
}
func (t FatherB) doEat() {
   fmt.Printf("FatherB eat")
}
type FatherB struct {
}
type FatherA struct  {
}

3.5.4 函数声明

Go代码调用Go编写的函数不需要声明,可以直接调用,但Go调用汇编语言编写的函数还是要使用函数声明语句
函数声明=函数名+函数签名

函数签名
func (InputTypeList)OutputTypeList
函数声明
func FuncName (InputTypeList)OutputTypeList
有名函数定义
func add(a,b int) int {
	return a + b
}
函数声明语句
func add(int,int) int
匿名函数不能独立存在,常作为函数参数,返回值,或者赋值给某个变量
匿名函数可以直接显式初始化
匿名函数的类型也是函数字面量类型func (int,int) int
func (a,b) int{
	return a + b
}
新定义**函数类型ADD**
ADD底层是**函数字面量func (int,int) int**
type ADD func (int,int) int
add和ADD底层类型相同,且add是字面量类型
var g ADD = add
func main() {
	f:=func (a,b int) int{
		return a+b
	} 
	g(1,2)
	f(1,2)
	fmt.Printf("%T\n",f) //func (int,int) int
	fmt.Printf("%T\n",add) //func (int,int) int
}

字面量类型是一种未命名类型,不能增加自己的方法,必须显式使用type进行声明
“函数类型”,“函数签名”不是等价,两者底层类型相同,当其中一个是字面量类型可以相互转换

定义有名的函数类型HandlerFunc
type HandlerFunc func(ResponseWriter,*Request)
为有名的函数类型添加方法,是一种包装器的编程技法
func (f HandlerFunc) ServerHttp(w ResponseWriter,r *Request) {
	f(w,r)
}
函数类型HandlerFunc实现了接口Handler
type Handler interface {
	ServerHttp(ResponseWriter,*Request)
}
func (mux *ServerMux) Handle(pattern string,handler Handler)
所以HandlerFunc类型的变量可以传递给Handler接口变量
func (mux *ServerMux) HandlerFunc(pattern string,handler func(ResponseWriter,*Request)) 
{
	mux.Handle(pattern,HandlerFunc(handler))
}

函数类型意义:
1.函数也是一种类型,可以在函数字面量的基础上使用type定义命名函数类型。
2.有名函数和匿名函数的函数签名和命名函数的底层类型相同,可以进行类型转换。
3.可以为有名函数类型添加方法。
4.为有名函数类型添加方法,使其与接口打通关系,使用接口的地方可以传递函数类型的变量。

  • 9
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值