上一篇博客介绍了 Go 指针的相关内容,本篇博客主要分享 Go 结构体和指针。

结构体定义

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

实例:

type Rect struct {
	width float64
	length float64
}

上述代码定义了一个矩形结构体,首先是关键是 type 表示要定义一个新的数据类型了,然后是新的数据类型名称 Rect,最后是 struct 关键字,表示这个高级数据类型是结构体类型。在上面的例子中,因为 width 和 length 的数据类型相同,还可以写成如下格式:

type Rect struct {
	width,length float64
}

如果要访问结构体成员,需要使用点号 . 操作符,格式为:

结构体.成员名

实例:

type Rect struct {
	width, length float64
}

func main() {
	var rect Rect
	rect.width = 100
	rect.length = 200
	fmt.Println(rect.width * rect.length)
}

其实一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:

variable_name := Rect {100, 200}
或
variable_name := Rect { width: 100, length: 200}

因此上述实例代码可以写为:

type Rect struct {
	width, length float64
}

func main() {
	var rect = Rect{width: 100, length: 200}

	fmt.Println(rect.width * rect.length)
}
或
func main() {
	var rect = Rect{100, 200}

	fmt.Println("Width:", rect.width, "* Length:",
		rect.length, "= Area:", rect.width*rect.length)
}

结构体参数传递方式

Go 函数的参数传递方式是值传递,这句话对结构体也是适用的。

type Rect struct {
	width, length float64
}

func double_area(rect Rect) float64 {
	rect.width *= 2
	rect.length *= 2
	return rect.width * rect.length
}
func main() {
	var rect = Rect{100, 200}
	fmt.Println(double_area(rect))
	fmt.Println("Width:", rect.width, "Length:", rect.length)
}

也就说虽然在 double_area 函数里面我们将结构体的宽度和长度都加倍,但仍然没有影响 main 函数里面的 rect 变量的宽度和长度。

结构体组合函数

上面在 main 函数中计算了矩形的面积,但是仍旧觉得矩形的面积如果能够作为矩形结构体的“内部函数”提供会更好。这样就可以直接说这个矩形面积是多少,而不用另外去取宽度和长度去计算。现在看一下结构体“内部函数”定义方法:

type Rect struct {
	width, length float64
}

func (rect Rect) area() float64 {
	return rect.width * rect.length
}

func main() {
	var rect = Rect{100, 200}

	fmt.Println("Width:", rect.width, "Length:", rect.length,
		"Area:", rect.area())
}

虽然 main 函数中的 rect 变量可以直接调用函数 area() 来获取矩形面积,但是 area() 函数确实没有定义在 Rect 结构体内部,这点和 C 语言的有很大不同。Go 使用组合函数的方式来为结构体定义结构体方法。仔细看一下上面的 area() 函数定义。

首先是关键字 func 表示这是一个函数,第二个参数是结构体类型和实例变量,第三个是函数名称,第四个是函数返回值。这里可以看出 area() 函数和普通函数定义的区别就在于 area() 函数多了一个结构体类型限定。这样一来 Go 就知道了这是一个为结构体定义的方法。

这里需要注意一点就是定义在结构体上面的函数(function)一般叫做方法(method)。

结构体和指针

指针的主要作用就是在函数内部改变传递进来变量的值。

可以定义指向结构体的指针类似于其他指针变量,格式如下:

var rect *Rect

以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前:

rect = &Rect;

使用结构体指针访问结构体成员,使用 . 操作符:

rect.width

使不使用结构体指针和使不使用指针的出发点是一样的,那就是是否试图在函数内部改变传递进来的参数的值。举个例子如下:

type Rect struct {
	width, length float64
}

func (rect *Rect) double_area() float64 {
	rect.width *= 2
	rect.length *= 2
	return rect.width * rect.length
}

func main() {
	var rect = new(Rect)
	rect.width = 100
	rect.length = 200
	fmt.Println(*rect)
	fmt.Println("Double Width:", rect.width, "Double Length:", rect.length,
		"Double Area:", rect.double_area())
	fmt.Println(*rect)
}

结构体内嵌类型

结构体内部定义另外一个结构体类型的成员。例如 iPhone 也是 Phone,看下个例子:

type Phone struct {
	price int
	color string
}

type IPhone struct {
	phone Phone
	model string
}

func main() {
	var p IPhone
	p.phone.price = 5000
	p.phone.color = "Black"
	p.model = "iPhone 5"
	fmt.Println("I have a iPhone:")
	fmt.Println("Price:", p.phone.price)
	fmt.Println("Color:", p.phone.color)
	fmt.Println("Model:", p.model)
}

在结构体 IPhone 里面定义了一个 Phone 变量 phone,然后我们可以像正常的访问结构体成员一样访问 phone 的成员数据。但是我们原来的意思是 “iPhone也是(is-a)Phone”,而这里的结构体 IPhone 里面定义了一个 phone 变量,给人的感觉就是 “iPhone有一个(has-a)Phone”,挺奇怪的。当然 Go 也知道这种方式很奇怪,所以支持如下做法:

type Phone struct {
	price int
	color string
}

type IPhone struct {
	Phone
	model string
}

func main() {
	var p IPhone
	p.price = 5000
	p.color = "Black"
	p.model = "iPhone 5"
	fmt.Println("I have a iPhone:")
	fmt.Println("Price:", p.price)
	fmt.Println("Color:", p.color)
	fmt.Println("Model:", p.model)
}

在这个例子中,定义了 IPhone 结构体的时候,不再定义 Phone 变量,直接把结构体 Phone 类型定义在那里。然后 IPhone 就可以像访问直接定义在自己结构体里面的成员一样访问 Phone 的成员。

上面的例子中,演示了结构体的内嵌类型以及内嵌类型的成员访问,除此之外,假设结构体 A 内部定义了一个内嵌结构体 B,那么 A 同时也可以调用所有定义在 B 上面的函数。

type Phone struct {
	price int
	color string
}

func (phone Phone) ringing() {
	fmt.Println("Phone is ringing...")
}

type IPhone struct {
	Phone
	model string
}

func main() {
	var p IPhone
	p.price = 5000
	p.color = "Black"
	p.model = "iPhone 5"
	fmt.Println("I have a iPhone:")
	fmt.Println("Price:", p.price)
	fmt.Println("Color:", p.color)
	fmt.Println("Model:", p.model)

	p.ringing()
}

接口

接下来了解一下接口,先看一个例子,关于 Nokia 手机和 iPhone 手机都能够打电话的例子。

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
	fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
	fmt.Println("I am iPhone, I can call you!")
}
func main() {
	var nokia NokiaPhone
	nokia.call()

	var iPhone IPhone
	iPhone.call()
}

定义了 NokiaPhone 和 IPhone,它们都有各自的方法 call(),表示自己都能够打电话。但是再想一想,是手机都应该能够打电话,所以这个不算是 NokiaPhone 或是 IPhone 的独特特点。否则 iPhone 不可能卖这么贵了。

再仔细看一下接口的定义,首先是关键字 type,然后是接口名称,最后是关键字 interface 表示这个类型是接口类型。在接口类型里面,Go 定义了一组方法。

Go 语言提供了一种接口功能,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口,不一定非要显式地声明要去实现哪些接口啦。比如上面的手机的 call() 方法,就完全可以定义在接口 Phone 里面,而 NokiaPhone 和 IPhone 只要实现了这个接口就是一个 Phone。

type Phone interface {
	call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
	fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
	fmt.Println("I am iPhone, I can call you!")
}

func main() {
	var phone Phone

	phone = new(NokiaPhone)
	phone.call()

	phone = new(IPhone)
	phone.call()

}

以上代码定义了一个接口 Phone,接口里面有一个方法 call(),仅此而已。然后在 main 函数里面定义了一个 Phone 类型变量,并分别为之赋值为 NokiaPhone 和 IPhone。

Go 语言式静态类型语言,变量的类型在运行过程中不能改变。在 Go 语言里面,一个类型 A 只要实现了接口 X 所定义的全部方法,那么 A 类型的变量也是 X 类型的变量。在上面的例子中,NokiaPhone 和 IPhone 都实现了 Phone 接口的 call() 方法,所以它们都是 Phone,这样一来是不是感觉正常了一些。

为 Phone 添加一个方法 sales(),再来熟悉一下接口用法。

type Phone interface {
	call()
	sales() int
}

type NokiaPhone struct {
	price int
}

func (nokiaPhone NokiaPhone) call() {
	fmt.Println("I am Nokia, I can call you!")
}
func (nokiaPhone NokiaPhone) sales() int {
	return nokiaPhone.price
}

type IPhone struct {
	price int
}

func (iPhone IPhone) call() {
	fmt.Println("I am iPhone, I can call you!")
}

func (iPhone IPhone) sales() int {
	return iPhone.price
}

func main() {
	var phones = [5]Phone{
		NokiaPhone{price: 350},
		IPhone{price: 5000},
		IPhone{price: 3400},
		NokiaPhone{price: 450},
		IPhone{price: 5000},
	}

	var totalSales = 0
	for _, phone := range phones {
		totalSales += phone.sales()
	}
	fmt.Println(totalSales)
}

上面的例子中,定义了一个手机数组,然后计算手机的总售价。可以看到,由于 NokiaPhone 和 IPhone 都实现了 sales() 方法,所以它们都是 Phone 类型,但是计算售价的时候,Go 会知道调用哪个对象实现的方法。

接口类型还可以作为结构体的数据成员。

假设下列情况,儿子在 iPhone 没有出的时候,买了好几款 Nokia,iPhone 出来后,又买了好多部 iPhone,老爸要来看看这小子一共花了多少钱。

type Phone interface {
	sales() int
}

type NokiaPhone struct {
	price int
}

func (nokiaPhone NokiaPhone) sales() int {
	return nokiaPhone.price
}

type IPhone struct {
	price int
}

func (iPhone IPhone) sales() int {
	return iPhone.price
}

type Person struct {
	phones []Phone
	name   string
	age    int
}

func (person Person) total_cost() int {
	var sum = 0
	for _, phone := range person.phones {
		sum += phone.sales()
	}
	return sum
}

func main() {
	var bought_phones = [5]Phone{
		NokiaPhone{price: 350},
		IPhone{price: 5000},
		IPhone{price: 3400},
		NokiaPhone{price: 450},
		IPhone{price: 5000},
	}

	var person = Person{name: "Jemy", age: 25, phones: bought_phones[:]}

	fmt.Println(person.name)
	fmt.Println(person.age)
	fmt.Println(person.total_cost())
}

这个例子纯为演示接口作为结构体数据成员。这里面定义了一个 Person 结构体,结构体内部定义了一个手机类型切片。另外定义了 Person 的 total_cost() 方法用来计算手机花费总额。

参考

https://www.kancloud.cn/itfanr/go-quick-learn/81641

http://www.runoob.com/go/go-structures.html