[toc]
Go类型
用户自定义类型
声明一个类型的时,这个声明就给编译器提供一个框架,告知必要的内存大小和表示信息。声明后与内置类型的运作方式类型。
需要注意得是,不同类型即使相互兼容,但是也不能相互赋值
// 定义一个新的类型
type user struct {
name string
email string
ext int
privileged bool
}
// 类型可以嵌套
type admin struct {
persion user
level string
}
初始化与赋值
// 默认值为新类型里属性对应的默认值
var bill user
fmt.Println(bill)
//类型创建并初始化
newUser := user{
name: "smith",
email: "smith@example.com",
ext: 0,
privileged: true,
}
fmt.Println(newUser)
//类型创建并初始化的另外一种方式,需要注意值得顺序
newUser2 := user{"smith", "smith@example.com", 0, false}
fmt.Println(newUser2)
方法
方法能给用户定义得类型添加新的行为。
在定义方法的时候,关键字func和函数名之间的参数被称作为接受者,将函数与接受者的类型绑定在一起。如果一个函数有接受者,那么这个函数就被称作为方法。
Go中有两种接受者: 值接受者和指针接收者。
如果使用值接收者,那么调用时会使用这个值的一个副本来执行
type user struct {
name string
email string
ext int
privileged bool
}
// 为user添加修改名字的方法,值接受者
func (u user) changeName(name string) {
u.name = name
}
// 指针接受者
func (u *user) changeEmail(email string) {
u.email = email
}
值得注意的是,值接受者使用值得副本来调用方法,而指针接受者使用实际的值调用方法,也可以使用一个值来调用使用指针接受者声明的方法。
newUser.changeName("smith1")
fmt.Println(newUser)
//(&newUser).changeEmail("smith1@example.com"),两者是等价的
newUser.changeEmail("smith1@example.com")
fmt.Println(newUser)
(&newUser).changeEmail("smith2@example.com")
fmt.Println(newUser)
newUser4 := &user{"smith", "smith@example.com", 0, false}
newUser4.changeName("123")
fmt.Println(newUser4)
什么时候使用指针接受者,什么时候使用值接受者
首先需要明白如果给某个类型增加或者删除某个值,是要修改当前值,还是更新当前值?如果是要创建一个新值,该类型的方法就使用值接受者,如果要修改当前值,就使用指针接受者。
原则是不要关注某个方法如何处理值,而是关注这个值的本质是什么
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值。
- 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
何时使用值类型
1.如果接受者是一个 map,func 或者 chan,使用值类型(因为它们本身就是引用类型)。
2.如果接受者是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。
3.如果接受者是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接受者是一个简单地基本类型像是 int 和 string,使用值类型就好了。
一个值类型的接受者可以减少一定数量的垃圾生成,如果一个值被传入一个值类型接受者的方法,一个栈上的拷贝会替代在堆上分配内存(但不是保证一定成功),所以在没搞明白代码想干什么之前,别因为这个原因而选择值类型接受者。
何时使用指针类型
1.如果方法需要修改接受者,接受者必须是指针类型。
2.如果接受者是一个包含了 sync.Mutex 或者类似同步字段的结构体,接受者必须是指针,这样可以避免拷贝。
3.如果接受者是一个大的结构体或者数组,那么指针类型接受者更有效率。(多大算大呢?假设把接受者的所有元素作为参数传给方法,如果你觉得参数有点多,那么它就是大)。
4.从此方法中并发的调用函数和方法时,接受者可以被修改吗?一个值类型的接受者当方法调用时会创建一份拷贝,所以外部的修改不能作用到这个接受者上。如果修改必须被原始的接受者可见,那么接受者必须是指针类型。
5.如果接受者是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接受者,这样会增加程序的可读性
当你看完这个还是有疑虑,还是不知道该使用哪种接受者,那么记住使用指针接受者。
是使用值接受者还是指针接受者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。
只有一个例外,需要让类型值复合某个接口的时候,即便类型的本质是非原始的,也可以选择使用值接受者声明方法。这样做完全复合调用方法的机制。
ref:https://www.136.la/tech/show-933792.html
类型的本质
内置类型
- 数值类型
- 字符串类型
- 布尔类型
内置类型的本质是原始类型。
对于内置类型,对这些值进行增加或者删除的时候,会创建新值,把这些类型的值传递给方法或者函数的时候,应该传递一个对应值的副本。
引用类型
- 切片
- 映射
- 通道
- 接口
- 函数类型
引用类型创建的变量成为标头值。每个标头值包含一个指向底层数据结构的指针。因此通过复制一个引用类型的值的副本,本质上就是在共享底层数据结构。
每个引用类型还包含足以独特的字段,用于管理底层数据结构。标头值是为了复制而设计的,所以永远不要共享一个引用类型的值。
结构类型
结构类型用来描述一组数据值,这组值得本质可以是原始的,也可以是非原始的。
嵌入类型
嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型成为新的外部类型的内部类型。
type user struct {
name string
email string
}
type admin struct {
user // 内嵌
level string
}
user1 := user{"smith", "smith@email.com"}
ad := admin{
user: user1,
level: "super",
}
fmt.Println(ad)
内部类型的提升:对于外部类型来说,内部类型总是存在的。这就意味着,虽然没有指定内部类型对应的字段名,还是可以使用内部类型的类型名来范文内部类型的值。
即下面两者是等价的。
fmt.Println(ad.name) //smith
fmt.Println(ad.user.name) //smith
同样的,对于接口的值而言,因为内部类型的提升,内部类型实现的接口会自动地提升到外部类型,因此外部类型也同样实现了这个接口
// 定义接口
type notifier interface {
notify()
}
// 内部类型实现接口
func (u *user) notify() {
fmt.Printf("user interface name:%s email:%s\n", u.name, u.email)
}
// 外部类型使用内部类型地接口
ad.notify() //user interface name:smith email:smith@email.com
但是如果外部类型同样实现同样的接口或者使用同样的变量,内部类型就不会得到提升。
//重新定义admin
type admin struct {
user
email string // 与user有同样的字段
}
// admin实现接口
func (u *admin) notify() {
fmt.Printf("Admin interface name:%s email:%s\n", u.name, u.email)
}
user1 := user{"smith", "smith@email.com"}
ad := admin{
user: user1,
email: "super@test.com",
}
// 同样的字段或者变量,内部类型不会得到提升
fmt.Println(ad.email) //super@test.com
fmt.Println(ad.user.email) //smith@email.com
ad.notify() // Admin interface name:smith email:super@test.com
需要注意一点是,即便内部类型是未公开的,内部类型声明的字段是公开的。那么这些公共字段也可以通过外部类型去访问。