Go 学习笔记(三)结构 struct

本文原创地址:博客园骏马金龙go 基础系列:结构 struct

Go 语言不是一门面向对象的语言,没有对象和继承,也没有面向对象的多态、重写相关特性。

Go 所拥有的是数据结构,它可以关联方法。Go 也支持简单但高效的组合 (Composition),请搜索面向对象和组合。

虽然 Go 不支持面向对象,但 Go 通过定义数据结构的方式,也能实现与 Class 相似的功能。

1.png

一个简单的例子,定义一个 Animal 数据结构:

type Animal struct {
	name string
	speak string
}

这就像是定义了一个 class,有自己的属性。

2.png

在稍后,将会介绍如何向这个数据结构中添加方法,就像为类定义方法一样。不过现在,先简单介绍下数据结构。

数据结构的定义和初始化

除了 int、string 等内置的数据类型,我们可以定义 structure 来自定义数据类型。

创建数据结构最简单的方式:

bm_horse := Animal{
	name:"baima",
	speak:"neigh",
}

注意,上面最后一个逗号 "," 不能省略,Go 会报错,这个逗号有助于我们去扩展这个结构,所以习惯后,这是一个很好的特性。

上面bm_horse := Animal{}中,Animal 就像是一个类,这个声明和赋值的操作就像创建了一个 Animal 类的实例,也就是对象,其中对象名为bm_horse,它是这个实例的唯一标识符。这个对象具有属性 name 和 speak,它们是每个对象所拥有的 key,且它们都有自己的值。从面向对象的角度上考虑,这其实很容易理解。

还可以根据 Animal 数据结构再创建另外一个实例:

hm_horse := Animal{
	name:"heima",
	speak:"neigh",
}

bm_horsehm_horse都是 Animal 的实例,根据 Animal 数据结构创建而来,这两个实例都拥有自己的数据结构。如下图:

3.png

从另一种角度上看,bm_horse这个名称其实是这个数据结构的一个引用。再进一步考虑,其实面向对象的类和对象也是一种数据结构,每一个对象的名称 ( 即bm_horse) 都是对这种数据结构的引用。关于这一点,在后面介绍指针的时候将非常有助于理解。

以下是两外两种有效的数据结构定义方式:

// 定义空数据结构
bm_horse := Animal{}

// 或者,先定义一部分,再赋值
bm_horse := Animal {name:"baima"}
bm_horse.speak = "neigh"

此外,还可以省略数据结构中的 key 部分 (也就是属性的名称) 直接为数据结构中的属性赋值,只不过这时赋的值必须和 key 的顺序对应。

bm_horse := Animal{"baima","neigh"}

在数据结构的属性数量较少的时候,这种赋值方式也是不错的,但属性数量多了,不建议如此赋值,因为很容易混乱。

访问数据结构的属性

要访问一个数据结构中的属性,如下:

package main

import ("fmt")

func main(){
	
	type Animal struct {
		name string
		speak string
	}

	bm_horse := Animal{"baima","neigh"}
	fmt.Println("name:",bm_horse.name)
	fmt.Println("speak:",bm_horse.speak)
}

前面说过,Animal 是一个数据结构的模板 (就像类一样),不是实例,bm_horse才是具体的实例,有自己的数据结构,所以,要访问自己数据结构中的数据,可以通过自己的名称来访问自己的属性:

bm_horse.name
bm_horse.speak

指针

bm_horse := Animal{}表示返回一个数据结构给 bm_horse,bm_horse 指向这个数据结构,也可以说 bm_horse 是这个数据结构的引用。

除此,还有另一种赋值方式,比较下两种赋值方式:

bm_horse := Animal{"baima","neigh"}
ref_bm_horse := &Animal{"baima","neigh"}

这两种赋值方式,有何不同?

:=操作符都声明左边的变量,并赋值变量。赋值的内容基本神似:

  • 第一种将整个数据结构赋值给变量bm_horsebm_horse从此变成 Animal 的实例;
  • 第二种使用了一个特殊符号&在数据结构前面,它表示返回这个数据结构的引用,也就是这个数据结构的地址,所以ref_bm_horse也指向这个数据结构。

bm_horseref_bm_horse都指向这个数据结构,有什么区别?

实际上,赋值给bm_horse的是 Animal 实例的地址,赋值给ref_bm_horse是一个中间的指针,这个指针里保存了 Animal 实例的地址。它们的关系相当于:

bm_horse -> Animal{}
ref_bm_horse -> Pointer -> Animal{}

其中 Pointer 在内存中占用一个长度为一个机器字长的单独数据块,64 位机器上一个机器字长是 8 字节,所以赋值给ref_bm_horse的这个 8 字节长度的指针地址,这个指针地址再指向Animal{},而bm_horse则是直接指向Animal{}

如果还不明白,我打算用 perl 语言的语法来解释它们的区别,因为 C 和 Go 的指针太过 "晦涩"。

perl 中的引用

在 Perl 中,一个 hash 结构使用%符号来表示,例如:

%Animal = (
	name => "baima",
	speak => "neigh",
);

这里的 "Animal" 表示的是这个 hash 结构的名称,然后通过%+NAME的方式来引用这个 hash 数据结构。其实 hash 结构的名称 "Animal" 就是这个 hash 结构的一个引用,表示指向这个 hash 结构,只不过这个Animal是创建 hash 结构是就指定好的已命名的引用。

perl 中还支持显式地创建一个引用。例如:

$ref_myhash = \%Animal;

%Animal表示的是 hash 数据结构,加上\表示这个数据结构的一个引用,这个引用指向这个 hash 数据结构。perl 中的引用是一个变量,所以使用$ref_myhash表示。

4.png

也就是说,hash 结构的名称Animal$ref_myhash是完全等价的,都是 hash 结构的引用,也就是指向这个数据结构,也就是指针。所以,%Animal能表示取 hash 结构的属性,%$ref_myhash也能表示取 hash 结构的属性,这种从引用取回 hash 数据结构的方式称为 "解除引用"。

另外,$ref_myhash是一个变量类型,而%Animal是一个 hash 类型。

引用变量可以赋值给另一个引用变量,这样两个引用都将指向同一个数据结构:

$ref_myhash1 = $ref_myhash;

现在,$ref_myhash$ref_myhash1Animal都指向同一个数据结构。

Go 中的指针:引用

总结下上面 perl 相关的代码:

%Animal = (
	name => "baima",
	speak => "neigh",
);

$ref_myhash = \%Animal;
$ref_myhash1 = $ref_myhash;

%Animal是 hash 结构,Animal$ref_myhash$ref_myhash1都是这个 hash 结构的引用。

回到 Go 语言的数据结构:

bm_horse :=  Animal{}
hm_horse := &Animal{}

这里的Animal{}是一个数据结构,相当于 perl 中的 hash 数据结构:

(
	name => "baima",
	speak => "neigh",
)

bm_horse 是数据结构的直接赋值对象,它直接表示数据结构,所以它等价于前面 perl 中的%Animal。而hm_horseAnimal{}数据结构的引用,它等价于 perl 中的Animal$ref_myhash$ref_myhash1

之所以 Go 中的指针不好理解,就是因为数据结构 bm_horse 和引用 hm_horse 都没有任何额外的标注,看上去都像是一种变量。但其实它们是两种不同的数据类型:一种是数据结构,一种是引用。

Go 中的星号 "*"

星号有两种用法:

  • x *int表示变量 x 是一个引用,这个引用指向的目标数据是 int 类型。更通用的形式是x *TYPE
  • *x表示 x 是一个引用,*x表示解除这个引用,取回 x 所指向的数据结构,也就是说这是 一个数据结构,只不过这个数据结构可能是内置数据类型,也可能是自定义的数据结构

x *int的 x 是一个指向 int 类型的引用,而&y返回的也是一个引用,所以&y的 y 如果是 int 类型的数据,&y可以赋值给x *int的 x。

注意,x 的数据类型是*int,不是 int,虽然 x 所指向的是数据类型是 int。就像前面 perl 中的引用只是一个变量,而其指向的却是一个 hash 数据结构一样。

*x代表的是数据结构自身,所以如果为其赋值 ( 如*x = 2),则新赋的值将直接保存到 x 指向的数据中。

例如:

package main

import ("fmt")

func main(){
	var a *int
	c := 2
	a = &c
	d := *a
	fmt.Println(*a)   // 输出2
	fmt.Println(d)    // 输出2
}

var a *int定义了一个指向 int 类型的数据结构的引用。a = &c中,因为&c返回的是一个引用,指向的是数据结构 c,c 是 int 类型的数据结构,将其赋值给 a,所以 a 也指向 c 这个数据结构,也就是说*a的值将等于 2。所以d := *a赋值后,d 自身是一个 int 类型的数据结构,其值为 2。

Go 函数参数传值

Go 函数给参数传递值的时候是以复制的方式进行的

因为复制传值的方式,如果函数的参数是一个数据结构,将直接复制整个数据结构的副本传递给函数,这有两个问题:

  1. 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
  2. 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小

例如,第一个问题:

package main

import ("fmt")

type Animal struct {
	name string
	weight int
}

func main(){
	bm_horse := Animal{
		name: "baima",
		weight: 60,
	}
	add(bm_horse)
	fmt.Println(bm_horse.weight)
}

func add(a Animal){
	a.weight += 10
}

上面的输出结果仍然为 60。add 函数用于修改 Animal 的实例数据结构中的 weight 属性。当执行add(bm_horse)的时候,bm_horse传递给 add()函数,但并不是直接传递给 add() 函数,而是复制一份bm_horse的副本赋值给 add 函数的参数 a,所以 add() 中修改的a.weight的属性是bm_horse的副本,而不是直接修改的 bm_horse,所以上面的输出结果仍然为 60。

为了修改 bm_horse 所在的数据结构的值,需要使用引用 (指针) 的方式传值。

只需修改两个地方即可:

package main

import ("fmt")

type Animal struct {
	name string
	weight int
}

func main(){
	bm_horse := &Animal{
		name: "baima",
		weight: 60,
	}
	add(bm_horse)
	fmt.Println(bm_horse.weight)
}

func add(a *Animal){
	a.weight += 10
}

为了修改传递给函数参数的数据结构,这个参数必须是直接指向这个数据结构的。所以使用add(a *Animal),既然 a 是一个 Animal 数据结构的一个实例的引用,所以调用 add()的时候,传递给 add() 中的参数必须是一个 Animal 数据结构的引用,所以bm_horse的定义语句中使用&符号。

当调用到add(bm_horse)的时候,因为bm_horse是一个引用,所以赋值给函数参数 a 时,复制的是这个数据结构的引用,使得 add 能直接修改其外部的数据结构属性。

大多数时候,传递给函数的数据结构都是它们的引用,但极少数时候也有需求直接传递数据结构。

方法:属于数据结构的函数

可以为数据结构定义属于自己的函数。

package main
import ("fmt")

type Animal struct {
	name string
	weight int
}

func (a *Animal) add() {
	a.weight += 10
}

func main() {
	bm_horse := &Animal{"baima",70}
	bm_horse.add()
	fmt.Println(bm_horse.weight)    // 输出80
}

上面的 add() 函数定义方式func (a *Animal) add(){},它所表示的就是定义于数据结构 Animal 上的函数,就像类的实例方法一样,只要是属于这个数据结构的实例,都能直接调用这个函数,正如bm_horse.add()一样。

构造器

面向对象中有构造器 (也称为构造方法),可以根据类构造出类的实例:对象。

Go 虽然不支持面向对象,没有构造器的概念,但也具有构造器的功能,毕竟构造器只是一个方法而已。只要一个函数能够根据数据结构返回这个数据结构的一个实例对象,就可以称之为 "构造器"。

例如,以下是 Animal 数据结构的一个构造函数:

func newAnimal(n string,w int) *Animal {
	return &Animal{
		name: n,
		weight: w,
	}
}

以下返回的是非引用类型的数据结构:

func newAnimal(n string,w int) Animal {
	return Animal{
		name: n,
		weigth: w,
	}
}

一般上面的方法类型称为工厂方法,就像工厂一样根据模板不断生成产品。但对于创建数据结构的实例来说,一般还是会采用内置的 new() 方式。

new 函数

尽管 Go 没有构造器,但 Go 还有一个内置的 new() 函数用于为一个数据结构分配内存。其中new(x)等价于&x{},以下两语句等价:

bm_horse := new(Animal)
bm_horse := &Animal{}

使用哪种方式取决于自己。但如果要进行初始化赋值,一般采用第二种方法,可读性更强:

# 第一种方式
bm_horse := new(Animal)
bm_horse.name = "baima"
bm_horse.weight = 60

# 第二种方式
bm_horse := &Animal{
	name: "baima",
	weight: 60,
}

扩展数据结构的字段

在前面出现的数据结构中的字段数据类型都是简简单单的内置类型:string、int。但数据结构中的字段可以更复杂,例如可以是 map、array 等,还可以是自定义的数据类型 (数据结构)。

例如,将一个指向同类型数据结构的字段添加到数据结构中:

type Animal struct {
	name   string
	weight int
	father *Animal
}

其中在此处的*Animal所表示的数据结构实例很可能是其它的 Animal 实例对象。

上面定义了 father,还可以定义 son,sister 等等。

例如:

bm_horse := &Animal{
	name: "baima",
	weight: 60,
	father: &Animal{
		name: "hongma",
		weight: 80,
		father: nil,
	},
}

composition

Go 语言支持 Composition(组合),它表示的是在一个数据结构中嵌套另一个数据结构的行为。

package main

import (
	"fmt"
)

type Animal struct {
	name   string
	weight int
}

type Horse struct {
	*Animal                  // 注意此行
	speak string
}

func (a *Animal) hello() {
	fmt.Println(a.name)
	fmt.Println(a.weight)
	//fmt.Println(a.speak)
}

func main() {
	bm_horse := &Horse{
		Animal: &Animal{        // 注意此行
			name:   "baima",
			weight: 60,
		},
		speak: "neigh",
	}
	bm_horse.hello()
}

上面的 Horse 数据结构中包含了一行*Animal,表示 Animal 的数据结构插入到 Horse 的结构中,这就像是一种面向对象的类继承。注意,没有给该字段显式命名,但可以隐式地访问 Horse 组合结构中的字段和函数。

另外,在构建 Horse 实例的时候,必须显式为其指定字段名 (尽管数据结构中并没有指定其名称),且字段的名称必须和数据结构的名称完全相同。

然后调用属于 Animal 数据结构的 hello 方法,它只能访问 Animal 中的属性,所以无法访问 speak 属性。

很多人认为这种代码共享的方式比面向对象的继承更加健壮。

Go 中的重载 overload

例如,将上面属于 Animal 数据结构的 hello 函数重载为属于 Horse 数据结构的 hello 函数:

package main

import (
	"fmt"
)

type Animal struct {
	name   string
	weight int
}

type Horse struct {
	*Animal                  // 注意此行
	speak string
}

func (h *Horse) hello() {
	fmt.Println(h.name)
	fmt.Println(h.weight)
	fmt.Println(h.speak)
}

func main() {
	bm_horse := &Horse{
		Animal: &Animal{       // 注意此行
			name:   "baima",
			weight: 60,
		},
		speak: "neigh",
	}
	bm_horse.hello()
}

上一篇 Go 学习笔记(二)简介
Go 学习笔记(目录)
下一篇 Go 学习笔记(四)构建 go 程序