go语言是静态语言,这意味着编译器需要在编译时知道变量的类型。类型提供给编译期的信息:
- 内存规模:编译期得知该分配多少内存
- 内存表示信息:内存中存放的是何物
1. 自定义类型
1.1. 用户可以使用struct自定义结构
/* 代码片段 5.1 */
// 声明一个用户类型 name
type name struct {
firstname string
lastname string
}
// 声明一个name变量。使用变量的默认值初始化。数值:0;字符串:"";布尔类型:false
var name1 name
// 使用 结构字面量 方式1 声明并初始化
name2 := name{
firstname: "kris",
lastname: "who", // 每一行末尾需要逗号。值顺序不重要
}
// 使用 结构字面量 方式2 声明并初始化
name3 := name {"kris", "wu"} // 值的顺序重要
// 定义嵌套结构
type person struct {
_name name,
_sex string
}
1.2. 基于已有基础类型声明新类型
/* 代码片段 5.2 */
type Duration int64
Duration类型虽然使用int64类型表示,但编译器会将两者视为不同的类型。即使能相互兼容,但不能相互赋值。编译器不会对不同类型的值做隐式转化(这点和C++有点差异)。
2、方法
方法能给用户定义的类型添加新的行为。
/* 代码片段 5.3 */
// 以 代码片段 5.1 的name结构体为例
func (n name) getName() {
fmt.Printf("first name:%s, last name:%s", n.firstname, n.lastname)
}
func (n *name) getLastName() {
return n.lastname
}
上诉代码中func和函数名(getName)之间的参数被称为接收者,如果一个函数有接收者,那该函数就被称为方法。
接收者分为:值接收者 和 指针接收者。参数传递的规则和C++中的函数参数传递规则相同,即值接收者时,是通过值副本的方式传递;指针接收者时,通过传递指针的副本,但本质上指向的是同一块内存。
无论你在定义方法时使用的是值接收者还是指针接收者,都可以通过变量或变量指针来调用。即
/* 代码片段 5.4 */
// 使用 代码片段 5.1 的name结构体为例
name2 := name{
firstname: "kris",
lastname: "who",
}
name2.getName() // 可以调用成功
name2.getLastName() // 可以调用成功。编译器会将调用转化为(&name2).getLastName()
name3 := &name {"kris", "wu"}
name3.getName() // 可以调用成功。编译器会将调用转化为(*name3).getName()
name3.getLastName() // 可以调用成功
3. 类型的本质
当决定是要使用值传递的方式还是指针传递的方式时,要关注传递的值的本质。
3.1. 内置类型
语言提供的基础类型(如数值类型、字符串类型、布尔类型),本质上是原始的类型。因此,当对这些基础类型的值进行增加或删除的时候,会创建一个新值。所以应该传递一个对应值的副本。
3.2. 引用类型
go中引用类型包括:
- 切片
- 映射
- 通道
- 接口
- 函数类型
当创建一个引用类型时,创建的变量被称为标头(header)。其中包含一个指向底层基础数据结构的指针。因为标头值就是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递引用类型的值的副本,本质上就是在共享底层数据结构。引用类型的值在其他方面像原始的数据类型的值一样对待。
3.3. 结构类型
结构类型可以用来描述一组数据值,这组值的本质既可以是原始的,也可以是非原始的。
上诉是书中翻译的原话,个人感觉理解起来有点绕。本质是原始的类型可以理解为不允许在函数方法中被改动,即需要传递结构类型的值的副本;本质是非原始的类型可以理解为允许在函数方法中被改动,即需要传递结构类型的值的引用。(如果有更好的理解方法,请告知)
4. 接口
接口是go语言实现多态的方式。
对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。在这个关系中,用户定义的类型通常叫做实体类型
4.1. 实现
接口的内部实现方式类似于C++中的虚指针和虚表。接口变量的值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表称为iTable,包含了所存储值的类型信息以及该值所关联的一组方法(方法集)。第二个字是一个指向所存储值的指针。
4.2. 方法集
方法集定义了接口的接受规则。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
方法集的规则如下:
方法接收者 | 值 |
---|---|
(t T) | T 和 T* |
(t *T) | *T |
即如果使用了指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,则那个指针的值和指针都能够实现对应的接口。对比上面结构方法的定义和调用方式,两者代码形式差不多,但方法的参数和调用是编译器后自动帮助转化,而在实现多态的方法集中规则不同。
/* 代码片段 5.5 */
package main
import "fmt"
type notifier interface { // 相当于定义了虚基类
call()
}
type nokia struct {}
func (n *nokia) call() {
fmt.Println("i am nokia")
}
type iphone struct {}
func (i *iphone) call() { // 这里的接收者是指针类型,则只能接受指针类型的值
fmt.Println("i am iphone")
}
func notify(p notifier) { // 这是使用多态的方法
p.call()
}
func main() {
p1 := nokia{}
notify(&p1) // 根据方法集的规则,如果这里调用方式为notify(p1),则会报错
p2 := iphone{}
notify(&p2)
}
4.3. 嵌入类型(type embedding)
嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。相当于外部类型组合了内部类型的所有属性。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。
/* 代码片段 5.6 */
package main
import (
"fmt"
)
type name struct {
firstname string
lastname string
}
func (n *name) getName() string {
return "struct name: " + n.firstname + " " + n.lastname
}
type person struct {
name // 1. 嵌入类型的定义方式不同。嵌入类型只要声明这个类型的名字
sex string
}
func (p *person) getName() string {
return "struct person: " + p.firstname + " " + p.lastname
}
func main() {
p := person{
name: name{ // 2. 嵌入类型的声明方式不同
firstname: "huihuang",
lastname: "zhang",
},
sex: "female",
}
fmt.Println(p.firstname) // 3. 输出为 huihuang
fmt.Println(p.getName()) // 4. 输出为 struct person: huihuang zhang
fmt.Println(p.name.getName()) // 5. 输出为 struct name: huihuang zhang
p.firstname = "chen" // 6. 修改同名标识符
fmt.Println(p.firstname) // 输出为 chen
fmt.Println(p.getName()) // 输出为 struct person: chen zhang
}
代码片段5.6演示了嵌入类型的使用。对比嵌套结构,两者间差距很小。代码中1,2标识出了其中的差别。内部类型相关的标识符会提升到外部类型上见注释3和4。6中修改了同名的标识符,后面的输出可见原始的值已经被修改了。内部类型name相对于外部类型person就是被嵌入的类型被称为新的外部类型的内部类型。
4.4. 公开或未公开的标识符
其实本人认为这节涉及的内容更倾向于作用域方面,放在这一章不太合适在go语言中,一个包中对于声明的标识符的公开程度规则如下:
- 大写字母开头的标识符对包外公开,类似于public变量
- 小写字母开头的标识符对包外不公开,类似于private变量
这其中涉及嵌套结构,结构名为大写开头,则对外部可见,但其中的内部字段需要大写开头才能外部可见,具体例子见代码片段5.9下。
类比C++,对于private变量,我们可以提供一个public的get,set方法来操作该变量。
如go语言的工厂函数
/* 代码片段5.7 */
// 遵守package名字要与文件夹名字相同的潜规则
package counters
type counter int
/* 代码片段5.8 */
import (
"./counter"
"fmt"
)
func main() {
c := counters.counter(10) // 报错:cannot refer to unexported name counters.counter
fmt.Println(c)
}
上面代码可见小写开头的标识符对外不可见。可以在包counters中声明一个public的工厂函数。
/* 代码片段5.7+ */
// 遵守package名字要与文件夹名字相同的潜规则
package counters
type counter int
func New(value int) counter {
return counter(value)
}
/* 代码片段5.8+ */
import (
"./counter"
"fmt"
)
func main() {
c := counters.New(10)
fmt.Println(c)
}
这时候代码就可以正常执行了。
/* 代码片段 5.9 */
// 遵守package名字要与文件夹名字相同的潜规则
package entities
type Body struct {
arm int
leg int
}
/* 代码片段 5.10 */
// testpulic
import (
"./entities"
"fmt"
)
func main() {
b := entities {
arm: 2, // 报错:unknown field 'arm' in struct literal of type entities.Body
leg: 2, // 报错:unknown field 'leg' in struct literal of type entities.Body
}
fmt.Println("%v", b)
}
特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。
- 赞助本站
- 微信扫一扫
-
- 加入Q群
- QQ扫一扫
-
评论