The Way to Go(四)

结构和方法

结构体定义

Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。

组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

结构体是值类型,因此可以通过new 函数来创建。

结构体定义的一般方式如下:

1
2
3
4
5
type identifier struct {
field1 type1
field2 type2
...
}

type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。

结构体的字段可以是任何类型,甚至是结构体本身 ,也可以是函数或者接口 。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:

1
2
3
var s T
s.a = 5
s.b = 8

使用 new

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。

1
2
var t *T
t = new(T)

写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"

type struct1 struct {
i1 int
f1 float32
str string
}

func main() {
ms := new(struct1)
ms.i1 = 10
ms.f1 = 15.5
ms.str= "Chris"

fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}

输出:

1
2
3
4
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation)点号符 来引用结构体的字段:

1
2
3
4
5
type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i

初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:

1
2
ms := &struct1{10, 15.5, "Chris"}
// 此时ms的类型是 *struct1

或者

1
2
var ms struct1
ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用new (),这里值的顺序必须按照字段顺序来写。

表达式 new(Type)&Type{} 是等价的。

使用结构体的一个典型例子:

1
2
3
4
type Interval struct {
start int
end int
}

初始化方式:

1
2
3
intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)

在(A)中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。(B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。

下图说明了结构体类型实例和一个指向它的指针的内存布局:

1
type Point struct { x, y int }

使用 new 初始化:

作为结构体字面量初始化:

下面的例子显示了一个结构体 Person,一个方法,方法有一个类型为 *Person 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import (
"fmt"
"strings"
)

type Person struct {
firstName string
lastName string
}

func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}

func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

// 2—struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Woodward"
(*pers2).lastName = "Woodward" // 这是合法的
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

// 3—struct as a literal:
pers3 := &Person{"Chris","Woodward"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}

输出:

1
2
3
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD

在上面例子的第二种情况中,可以直接通过指针,像 pers2.lastName="Woodward" 这样给结构体字段赋值,没有像 C++ 中那样需要使用 -> 操作符,Go 会自动做这样的转换。

注意也可以通过解指针的方式来设置值:(*pers2).lastName = "Woodward"

结构体的内存布局

1
2
type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }

结构体转换

Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,同时需要注意其中非法赋值或转换引起的编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

type number struct {
f float32
}

type nr number // alias type

func main() {
a := number{5.0}
b := nr{5.0}
// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile-error: cannot use b (type nr) as type number in assignment
// needs a conversion:
var c = number(b)
fmt.Println(a, b, c)
}

输出:

1
{5} {5} {5}

使用自定义包中的结构体

下面的例子中,main.go 使用了一个结构体,它来自 struct_pack 下的包 structPack

1
2
3
4
5
6
7
# structPack.go
package structPack

type ExpStruct struct {
Mi1 int
Mf1 float32
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# main.go

package main
import (
"fmt"
"./struct_pack/structPack"
)

func main() {
struct1 := new(structPack.ExpStruct)
struct1.Mi1 = 10
struct1.Mf1 = 16.

fmt.Printf("Mi1 = %d\n", struct1.Mi1)
fmt.Printf("Mf1 = %f\n", struct1.Mf1)
}

输出:

1
2
Mi1 = 10
Mf1 = 16.000000

带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。

标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"reflect"
)

type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}

func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}

func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}

输出:

1
2
3
An important answer
The name of the thing
How much there are

匿名字段和内嵌结构体

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。

可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。

内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type A struct {
ax, ay int
}

type B struct {
A
bx, by float32
}

func main() {
b := B{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(b.A)
}

输出:

1
2
1 2 3 4
{1 2}

方法

什么是方法

在 Go 语言中,结构体就像是类的一种简化形式,Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型,因为接口是一个抽象定义。

最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。

类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。

定义方法的一般格式如下:

1
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

在方法名之前,func 关键字之后的括号中指定 receiver

如果 recv 是 receiver 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()

如果 recv 一个指针,Go 会自动解引用。

如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:

1
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }

recv 就像是面向对象语言中的 thisself,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 thisself 作为 receiver 的名字。

下面是一个结构体上的简单方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type TwoInts struct {
a int
b int
}

func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10

fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))

two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}

func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}

func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}

输出:

1
2
3
The sum is: 22
Add them to the param: 42
The sum is: 7

下面是非结构体类型上方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type IntVector []int

func (v IntVector) Sum() (s int) {
for _, x := range v {
s += x
}
return
}

func main() {
fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
}

函数个方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。

方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

指针或值作为接收者

鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。

如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

下面的例子中,change()接受一个指向 B 的指针,并改变它内部的成员;write() 接受通过拷贝接受 B 的值并只输出B的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在指针上调用方法,Go 替我们做了这些事情。b1 是值而b2 是指针,方法都支持运行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
)

type B struct {
thing int
}

func (b *B) change() { b.thing = 1 }

func (b B) write() string { return fmt.Sprint(b) }

func main() {
var b1 B // b1是值
b1.change()
fmt.Println(b1.write())

b2 := new(B) // b2是指针
b2.change()
fmt.Println(b2.write())
}

/* 输出:
{1}
{1}
*/

试着在write() 中改变接收者b的值:将会看到它可以正常编译,但是开始的 b 没有被改变。

我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 Point3 的值来做计算:

1
2
3
4
5
type Point3 struct { x, y, z float64 }
// A method on Point3
func (p Point3) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}

这样做稍微有点昂贵,因为 Point3 是作为值传递给方法的,因此传递的是它的拷贝,这在 Go 中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。

假设 p3 定义为一个指针:p3 := &Point{ 3, 4, 5}

可以使用 p3.Abs() 来替代 (*p3).Abs()

在值和指针上调用方法:

可以有连接到类型的方法,也可以有连接到类型指针的方法。

但是这没关系:对于类型 T,如果在 *T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()

指针方法和值方法都可以在指针或非指针上被调用,如下面程序所示,类型 List 在值上有一个方法 Len(),在指针上有一个方法 Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

type List []int

func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }

func main() {
// 值
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1)

// 指针
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1)
}
------ 本文结束 ------
0%