本节我们要开讲 Go 语言在数据结构上最重要的概念 —— 结构体。如果说 Go 语言的基础类型是原子,那么结构体就是分子。分子是原子的组合,让形式有限的基础类型变化出丰富多样的形态结构。结构体里面装的是基础类型、切片、字典、数组以及其它类型的结构体等等。
结构体类型的定义
结构体和其它高级语言里的「类」比较类似。下面我们使用结构体语法来定义一个「圆」型
type Circle struct {
x int
y int
Radius int
}
结构体变量的创建
创建一个结构体变量有多种形式,我们先看结构体变量最常见的创建形式
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {
x: 100,
y: 100,
Radius: 50, // 注意这里的逗号不能少
}
fmt.Printf("%+v\n", c)
}
----------
{x:100 y:100 Radius:50}
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
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}
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {100, 100, 50}
fmt.Printf("%+v\n", c)
}
-------
{x:100 y:100 Radius:50}
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c *Circle = &Circle {100, 100, 50}
fmt.Printf("%+v\n", c)
}
-----------
&{x:100 y:100 Radius:50}
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c *Circle = new(Circle)
fmt.Printf("%+v\n", c)
}
----------
&{x:0 y:0 Radius:0}
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle
fmt.Printf("%+v\n", c)
}
var c1 Circle = Circle{}
var c2 Circle
var c3 *Circle = new(Circle)
零值结构体和 nil 结构体
nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。
var c *Circle = nil
结构体的内存大小
Go 语言的 unsafe 包提供了获取结构体内存占用的函数 Sizeof()
package main
import "fmt"
import "unsafe"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {Radius: 50}
fmt.Println(unsafe.Sizeof(c))
}
-------
24
结构体的拷贝
结构体之间可以相互赋值,它在本质上是一次浅拷贝操作,拷贝了结构体内部的所有字段。结构体指针之间也可以相互赋值,它在本质上也是一次浅拷贝操作,不过它拷贝的仅仅是指针地址值,结构体的内容是共享的。
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
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:50}
&{x:0 y:0 Radius:50}
无处不在的结构体
通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。
切片头的结构体形式如下,它在 64 位机器上将会占用 24 个字节
type slice struct {
array unsafe.Pointer // 底层数组的地址
len int // 长度
cap int // 容量
}
type string struct {
array unsafe.Pointer // 底层数组的地址
len int
}
type hmap struct {
count int
...
buckets unsafe.Pointer // hash桶地址
...
}
结构体中的数组和切片
在数组与切片章节,我们自习分析了数组与切片在内存形式上的区别。数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。请读者尝试解释一下下面代码的输出结果
package main
import "fmt"
import "unsafe"
type ArrayStruct struct {
value [10]int
}
type SliceStruct struct {
value []int
}
func main() {
var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}
-------------
80 24
结构体的参数传递
函数调用时参数传递结构体变量,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}
结构体方法
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
结构体的指针方法
如果使用上面的方法形式给 Circle 增加一个扩大半径的方法,你会发现半径扩大不了。
func (c Circle) expand() {
c.Radius *= 2
}
func (c *Circle) expand() {
c.Radius *= 2
}
通过指针访问内部的字段需要 2 次内存读取操作,第一步是取得指针地址,第二部是读取地址的内容,它比值访问要慢。但是在方法调用时,指针传递可以避免结构体的拷贝操作,结构体比较大时,这种性能的差距就会比较明显。
还有一些特殊的结构体它不允许被复制,比如结构体内部包含有锁时,这时就必须使用它的指针形式来定义方法,否则会发生一些莫名其妙的问题。
内嵌结构体
结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 Go 语言里称之为「组合」。下面我们来看看内嵌结构体的基本使用方法
package main
import "fmt"
type Point struct {
x int
y int
}
func (p Point) show() {
fmt.Println(p.x, p.y)
}
type Circle struct {
loc Point
Radius int
}
func main() {
var c = Circle {
loc: Point {
x: 100,
y: 100,
},
Radius: 50,
}
fmt.Printf("%+v\n", c)
fmt.Printf("%+v\n", c.loc)
fmt.Printf("%d %d\n", c.loc.x, c.loc.y)
c.loc.show()
}
----------------
{loc:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100
匿名内嵌结构体
还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,就好像把子结构体的一切全部都揉进了父结构体一样。匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称
package main
import "fmt"
type Point struct {
x int
y int
}
func (p Point) show() {
fmt.Println(p.x, p.y)
}
type Circle struct {
Point // 匿名内嵌结构体
Radius int
}
func main() {
var c = Circle {
Point: Point {
x: 100,
y: 100,
},
Radius: 50,
}
fmt.Printf("%+v\n", c)
fmt.Printf("%+v\n", c.Point)
fmt.Printf("%d %d\n", c.x, c.y) // 继承了字段
fmt.Printf("%d %d\n", c.Point.x, c.Point.y)
c.show() // 继承了方法
c.Point.show()
}
-------
{Point:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100
100 100
100 100
Go 语言的结构体没有多态性
Go 语言不是面向对象语言在于它的结构体不支持多态,它不能算是一个严格的面向对象语言。多态是指父类定义的方法可以调用子类实现的方法,不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。下面的例子呈现了 Java 类的多态性。
class Fruit {
public void eat() {
System.out.println("eat fruit");
}
public void enjoy() {
System.out.println("smell first");
eat();
System.out.println("clean finally");
}
}
class Apple extends Fruit {
public void eat() {
System.out.println("eat apple");
}
}
class Banana extends Fruit {
public void eat() {
System.out.println("eat banana");
}
}
public class Main {
public static void main(String[] args) {
Apple apple = new Apple();
Banana banana = new Banana();
apple.enjoy();
banana.enjoy();
}
}
----------------
smell first
eat apple
clean finally
smell first
eat banana
clean finally
Go 语言的结构体明确不支持这种形式的多态,外结构体的方法不能覆盖内部结构体的方法。比如我们用 Go 语言来改写上面的水果例子观察一下输出结果。
package main
import "fmt"
type Fruit struct {}
func (f Fruit) eat() {
fmt.Println("eat fruit")
}
func (f Fruit) enjoy() {
fmt.Println("smell first")
f.eat()
fmt.Println("clean finally")
}
type Apple struct {
Fruit
}
func (a Apple) eat() {
fmt.Println("eat apple")
}
type Banana struct {
Fruit
}
func (b Banana) eat() {
fmt.Println("eat banana")
}
func main() {
var apple = Apple {}
var banana = Banana {}
apple.enjoy()
banana.enjoy()
}
----------
smell first
eat fruit
clean finally
smell first
eat fruit
clean finally
面向对象的多态性需要通过 Go 语言的接口特性来模拟,这就是下一节我们要讲的主题。