结构和方法
结构体定义
Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。
组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
结构体是值类型,因此可以通过new
函数来创建。
结构体定义的一般方式如下:
1 | type identifier struct { |
type T struct {a, b int}
也是合法的语法,它更适用于简单的结构体。
结构体的字段可以是任何类型,甚至是结构体本身 ,也可以是函数或者接口 。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:
1 | var s T |
使用 new
使用 new
函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)
,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
1 | var t *T |
写这条语句的惯用方法是:t := new(T)
,变量 t
是一个指向 T
的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T
也会给 t
分配内存,并零值化内存,但是这个时候 t
是类型T
。在这两种方式中,t
通常被称做类型 T
的一个实例(instance)或对象(object)。
1 | package main |
输出:1
2
3
4The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}
无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation)点号符 来引用结构体的字段:
1 | type myStruct struct { i int } |
初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:
1 | ms := &struct1{10, 15.5, "Chris"} |
或者1
2var ms struct1
ms = struct1{10, 15.5, "Chris"}
混合字面量语法(composite literal syntax)&struct1{a, b, c}
是一种简写,底层仍然会调用new ()
,这里值的顺序必须按照字段顺序来写。
表达式 new(Type)
和 &Type{}
是等价的。
使用结构体的一个典型例子:1
2
3
4type Interval struct {
start int
end int
}
初始化方式:1
2
3intr := 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 | package main |
输出:1
2
3The 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 | type Rect1 struct {Min, Max Point } |
结构体转换
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,同时需要注意其中非法赋值或转换引起的编译错误。
1 | package main |
输出:1
{5} {5} {5}
使用自定义包中的结构体
下面的例子中,main.go
使用了一个结构体,它来自 struct_pack
下的包 structPack
。
1 | # structPack.go |
1 | # main.go: |
输出:1
2Mi1 = 10
Mf1 = 16.000000
带标签的结构体
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。
标签的内容不可以在一般的编程中使用,只有包 reflect
能获取它。它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf()
可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field
来索引结构体的字段,然后就可以使用 Tag
属性。
1 | package main |
输出:1
2
3An important answer
The name of the thing
How much there are
匿名字段和内嵌结构体
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。
1 | package main |
输出:
1 | 1 2 3 4 |
方法
什么是方法
在 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
就像是面向对象语言中的 this
或 self
,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this
或 self
作为 receiver
的名字。
下面是一个结构体上的简单方法的例子:
1 | package main |
输出:1
2
3The sum is: 22
Add them to the param: 42
The sum is: 7
下面是非结构体类型上方法的例子:
1 | package main |
函数个方法的区别
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
接收者必须有一个显式的名字,这个名字必须在方法中被使用。
receiver_type
叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。
在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
指针或值作为接收者
鉴于性能的原因,recv
最常见的是一个指向 receiver_type
的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver
类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
下面的例子中,change()
接受一个指向 B 的指针,并改变它内部的成员;write()
接受通过拷贝接受 B 的值并只输出B的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在指针上调用方法,Go 替我们做了这些事情。b1
是值而b2
是指针,方法都支持运行了。
1 | package main |
试着在write()
中改变接收者b
的值:将会看到它可以正常编译,但是开始的 b
没有被改变。
我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 Point3
的值来做计算:
1 | type Point3 struct { x, y, z float64 } |
这样做稍微有点昂贵,因为 Point3
是作为值传递给方法的,因此传递的是它的拷贝,这在 Go 中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。
假设 p3
定义为一个指针:p3 := &Point{ 3, 4, 5}
。
可以使用 p3.Abs()
来替代 (*p3).Abs()
。
在值和指针上调用方法:
可以有连接到类型的方法,也可以有连接到类型指针的方法。
但是这没关系:对于类型 T
,如果在 *T
上存在方法 Meth()
,并且 t
是这个类型的变量,那么 t.Meth()
会被自动转换为 (&t).Meth()
。
指针方法和值方法都可以在指针或非指针上被调用,如下面程序所示,类型 List 在值上有一个方法 Len()
,在指针上有一个方法 Append()
,但是可以看到两个方法都可以在两种类型的变量上被调用。
1 | package main |