一、结构体
《快学 Go 语言》第 8 课 —— 结构体
1.结构体类型的定义
结构体和其它高级语言里的「类」比较类似。下面我们使用结构体语法来定义一个「圆」型
type Circle struct {
x int
y int
Radius int
}
Circle 结构体内部有三个变量,分别是圆心的坐标以及半径。特别需要注意是结构体内部变量的大小写,首字母大写是公开变量,首字母小写是内部变量,分别相当于类成员变量的 Public 和 Private 类别。内部变量只有属于同一个 package(简单理解就是同一个目录)的代码才能直接访问。
2.创建
func main() {
var c Circle = Circle {
x: 100,
y: 100,
Radius: 50, // 注意这里的逗号不能少
}
fmt.Printf("%+v\n", c)
}
----------
{x:100 y:100 Radius:50}
可以只指定部分字段的初值,甚至可以一个字段都不指定,那些没有指定初值的字段会自动初始化为相应类型的「零值」。
func main() {
var c1 Circle = Circle {
Radius: 50,
}
var c2 Circle = Circle {}
fmt.Printf("%+v\n", c1)
fmt.Printf("%+v\n", c2)
}
----------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:0}
结构体的第二种创建形式是不指定字段名称来顺序字段初始化,需要显示提供所有字段的初值,一个都不能少。这种形式称之为「顺序形式」。var c Circle = Circle {100, 100, 50}
结构体变量创建的第三种形式,使用全局的 new() 函数来创建一个「零值」结构体,所有的字段都被初始化为相应类型的零值。var c *Circle = new(Circle)
注意 new() 函数返回的是指针类型。
第四种创建形式,这种形式也是零值初始化,就数它看起来最不雅观。var c Circle
3.零值结构体和 nil 结构体
nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。
var c *Circle = nil
而零值结构体是会实实在在占用内存空间的,只不过每个字段都是零值。如果结构体里面字段非常多,那么这个内存空间占用肯定也会很大。
4.结构体的拷贝
func main() {
var c1 Circle = Circle {Radius: 50}
var c2 Circle = c1
fmt.Printf("%+v\n", c1)
fmt.Printf("%+v\n", c2)
c1.Radius = 100
fmt.Printf("%+v\n", c1)
fmt.Printf("%+v\n", c2)
var c3 *Circle = &Circle {Radius: 50}
var c4 *Circle = c3
fmt.Printf("%+v\n", c3)
fmt.Printf("%+v\n", c4)
c3.Radius = 100
fmt.Printf("%+v\n", c3)
fmt.Printf("%+v\n", c4)
}
---------------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:100}
{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:100}
&{x:0 y:0 Radius:100}
5.无处不在的结构体
通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。
切片头的结构体形式如下,它在 64 位机器上将会占用 24 个字节
type slice struct {
array unsafe.Pointer // 底层数组的地址
len int // 长度
cap int // 容量
}
字符串头的结构体形式,它在 64 位机器上将会占用 16 个字节
type string struct {
array unsafe.Pointer // 底层数组的地址
len int
}
字典头的结构体形式
type hmap struct {
count int
...
buckets unsafe.Pointer // hash桶地址
...
}
6.结构体的参数传递
函数调用时参数传递结构体变量,Go 语言支持值传递,也支持指针传递。值传递涉及到结构体字段的浅拷贝,指针传递会共享结构体内容,只会拷贝指针地址,规则上和赋值是等价的。下面我们使用两种传参方式来编写扩大圆半径的函数。
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func expandByValue(c Circle) {
c.Radius *= 2
}
func expandByPointer(c *Circle) {
c.Radius *= 2
}
func main() {
var c = Circle {Radius: 50}
expandByValue(c)
fmt.Println(c)
expandByPointer(&c)
fmt.Println(c)
}
---------
{0 0 50}
{0 0 100}
从上面的输出中可以看到通过值传递,在函数里面修改结构体的状态不会影响到原有结构体的状态,函数内部的逻辑并没有产生任何效果。通过指针传递就不一样。
7.结构体方法
Go 语言不是面向对象的语言,它里面不存在类的概念,结构体正是类的替代品。类可以附加很多成员方法,结构体也可以。
package main
import "fmt"
import "math"
type Circle struct {
x int
y int
Radius int
}
// 面积
func (c Circle) Area() float64 {
return math.Pi * float64(c.Radius) * float64(c.Radius)
}
// 周长
func (c Circle) Circumference() float64 {
return 2 * math.Pi * float64(c.Radius)
}
func main() {
var c = Circle {Radius: 50}
fmt.Println(c.Area(), c.Circumference())
// 指针变量调用方法形式上是一样的
var pc = &c
fmt.Println(pc.Area(), pc.Circumference())
}
-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793
Go 语言不喜欢类型的隐式转换,所以需要将整形显示转换成浮点型,不是很好看,不过这就是 Go 语言的基本规则,显式的代码可能不够简洁,但是易于理解。
Go 语言的结构体方法里面没有 self 和 this 这样的关键字来指代当前的对象,它是用户自己定义的变量名称,通常我们都使用单个字母来表示。
Go 语言的方法名称也分首字母大小写,它的权限规则和字段一样,首字母大写就是公开方法,首字母小写就是内部方法,只能归属于同一个包的代码才可以访问内部方法。
结构体的值类型和指针类型访问内部字段和方法在形式上是一样的。这点不同于 C++ 语言,在 C++ 语言里,值访问使用句点 . 操作符,而指针访问需要使用箭头 -> 操作符。
8.关于GO如何实现面对对象的继承、多态,是个有趣的话题。参考go是面向对象语言吗?
9.创建递归的数据结构
《go语言圣经》P145
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:
type tree struct {
value int
left, right *tree
}
// Sort sorts values in place.
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}
// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
// Equivalent to return &tree{value: value}.
t = new(tree)
t.value = value
return t
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}
10.结构体的比较
《go语言圣经》P147
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:
type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q) // "false"
11.匿名结构体
《go语言圣经》P149
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。
不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:
gopl.io/ch4/embed
w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)
Output:Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员
w.X = 8 // equivalent to w.circle.point.X = 8
但是在包外部,因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。
到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。
二、接口
1.接口定义
Go 语言的接口类型非常特别,它的作用和 Java 语言的接口一样,但是在形式上有很大的差别。Java 语言需要在类的定义上显式实现了某些接口,才可以说这个类具备了接口定义的能力。但是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。下面我们看个例子
package main
import "fmt"
// 可以闻
type Smellable interface {
smell()
}
// 可以吃
type Eatable interface {
eat()
}
// 苹果既可能闻又能吃
type Apple struct {}
func (a Apple) smell() {
fmt.Println("apple can smell")
}
func (a Apple) eat() {
fmt.Println("apple can eat")
}
// 花只可以闻
type Flower struct {}
func (f Flower) smell() {
fmt.Println("flower can smell")
}
func main() {
var s1 Smellable
var s2 Eatable
var apple = Apple{}
var flower = Flower{}
s1 = apple
s1.smell()
s1 = flower
s1.smell()
s2 = apple
s2.eat()
}
--------------------
apple can smell
flower can smell
apple can eat
上面的代码定义了两种接口,Apple 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。我们并没有使用类似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。
2.空接口
如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。
Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{} ,初学者会非常不习惯。之所以这个类型名带上了大括号,那是在告诉用户括号里什么也没有。我始终认为这种名字很古怪,它让代码看起来有点丑陋。
空接口里面没有方法,所以它也不具有任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。
package main
import "fmt"
func main() {
// 连续两个大括号,是不是看起来很别扭
var user = map[string]interface{}{
"age": 30,
"address": "Beijing Tongzhou",
"married": true,
}
fmt.Println(user)
// 类型转换语法来了
var age = user["age"].(int)
var address = user["address"].(string)
var married = user["married"].(bool)
fmt.Println(age, address, married)
}
-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true
代码中 user 字典变量的类型是 map[string]interface{},从这个字典中直接读取得到的 value 类型是 interface{},需要通过类型转换才能得到期望的变量。
3.用接口来模拟多态
package main
import "fmt"
type Fruitable interface {
eat()
}
type Fruit struct {
Name string // 属性变量
Fruitable // 匿名内嵌接口变量
}
func (f Fruit) want() {
fmt.Printf("I like ")
f.eat() // 外结构体会自动继承匿名内嵌变量的方法
}
type Apple struct {}
func (a Apple) eat() {
fmt.Println("eating apple")
}
type Banana struct {}
func (b Banana) eat() {
fmt.Println("eating banana")
}
func main() {
var f1 = Fruit{"Apple", Apple{}}
var f2 = Fruit{"Banana", Banana{}}
f1.want()
f2.want()
}
---------
I like eating apple
I like eating banana
使用这种方式模拟多态本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性的结构体。
4.接口的组合继承
接口的定义也支持组合继承,比如我们可以将两个接口定义合并为一个接口如下
type Smellable interface {
smell()
}
type Eatable interface {
eat()
}
type Fruitable interface {
Smellable
Eatable
}
这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。
type Fruitable interface {
smell()
eat()
}
5.接口变量的赋值
变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不一样呢?接下来我们做一个实验
package main
import "fmt"
type Rect struct {
Width int
Height int
}
func main() {
var a interface {}
var r = Rect{50, 50}
a = r
var rx = a.(Rect)
r.Width = 100
r.Height = 100
fmt.Println(rx)
}
------
{50 50}
有疑问加站长微信联系(非本文作者)