结构体定义的一般方式如下:
~~~
type identifier struct {
field1 type1
field2 type2
...
}
~~~
`type T struct {a, b int}` 也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 **名字**,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 **_**。
结构体的字段可以是任何类型,甚至是结构体本身(参考第 [10.5](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/10.5.md) 节),也可以是函数或者接口(参考第 11 章)。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:
~~~
var s T
s.a = 5
s.b = 8
~~~
数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。
**使用 new**
使用 **new** 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:`var t *T = new(T)`,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
~~~
var t *T
t = new(T)
~~~
写这条语句的惯用方法是:`t := new(T)`,变量 `t` 是一个指向 `T`的指针,此时结构体字段的值是它们所属类型的零值。
声明 `var t T` 也会给 `t` 分配内存,并零值化内存,但是这个时候 `t` 是类型T。在这两种方式中,`t` 通常被称做类型 T 的一个实例(instance)或对象(Object)。
示例 10.1 [structs_fields.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_10/structs_fields.go) 给出了一个非常简单的例子:
~~~
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)
}
~~~
输出:
~~~
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}
~~~
使用 `fmt.Println` 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 **%v** 选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:`structname.fieldname = value`。
同样的,使用点号符可以获取结构体字段的值:`structname.fieldname`。
在 Go 语言中这叫 **选择器(selector)**。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 **选择器符(selector-notation)** 来引用结构体的字段:
~~~
type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i
~~~
初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:
~~~
ms := &struct1{10, 15.5, "Chris"}
// 此时ms的类型是 *struct1
~~~
或者:
~~~
var mt struct1
ms := struct1{10, 15.5, "Chris"}
~~~
混合字面量语法(composite literal syntax)`&struct1{a, b, c}` 是一种简写,底层仍然会调用 `new ()`,这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 `new(Type)`和 `&Type{}` 是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:
~~~
type Interval struct {
start int
end int
}
~~~
初始化方式:
~~~
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
~~~
在(A)中,值必须以字段在结构体定义时的顺序给出,**&** 不是必须的。(B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。
结构体类型和字段的命名遵循可见性规则(第 [4.2](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.2.md) 节),一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。
下图说明了结构体类型实例和一个指向它的指针的内存布局:
~~~
type Point struct { x, y int }
~~~
使用 new 初始化:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/eBook/images/10.1_fig10.1-1.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.1_fig10.1-1.jpg?raw=true)
作为结构体字面量初始化:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/eBook/images/10.1_fig10.1-2.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.1_fig10.1-2.jpg?raw=true)
类型 strcut1 在定义它的包 pack1 中必须是唯一的,它的完全类型名是:`pack1.struct1`。
下面的例子 [Listing 10.2—person.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/person.go) 显示了一个结构体 Person,一个方法,方法有一个类型为 `*Person` 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:
~~~
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)
}
~~~
输出:
~~~
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"`
**结构体的内存布局**
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。下面的例子清晰地说明了这些情况:
~~~
type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }
~~~
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/eBook/images/10.1_fig10.2.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.1_fig10.2.jpg?raw=true)
**递归结构体**
结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 `su`,树中的 `ri` 和 `le` 分别是指向别的节点的指针。
链表:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/eBook/images/10.1_fig10.3.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.1_fig10.3.jpg?raw=true)
这块的 `data` 字段用于存放有效数据(比如 float64),`su` 指针指向后继节点。
Go 代码:
~~~
type Node struct {
data float64
su *Node
}
~~~
链表中的第一个元素叫 `head`,它指向第二个元素;最后一个元素叫 `tail`,它没有后继元素,所以它的 `su` 为 nil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。
同样地可以定义一个双向链表,它有一个前趋节点 `pr` 和一个后继节点 `su`:
~~~
type Node struct {
pr *Node
data float64
su *Node
}
~~~
二叉树:
[![](https://github.com/Unknwon/the-way-to-go_ZH_CN/raw/master/eBook/images/10.1_fig10.4.jpg?raw=true)](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/images/10.1_fig10.4.jpg?raw=true)
二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(**root**),底层没有子节点的节点叫叶子节点(**leaves**),叶子节点的 `le` 和 `ri` 指针为 nil 值。在 Go 中可以如下定义二叉树:
~~~
type Tree strcut {
le *Tree
data float64
ri *Tree
}
~~~
**结构体转换**
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
示例 10.3:
~~~
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)
}
~~~
输出:
~~~
{5} {5} {5}
~~~
**练习 10.1** vcard.go:
定义结构体 Address 和 VCard,后者包含一个人的名字、地址编号、出生日期和图像,试着选择正确的数据类型。构建一个自己的 vcard 并打印它的内容。
~~~
提示:
VCard 必须包含住址,它应该以值类型还是以指针类型放在 VCard 中呢?
第二种会好点,因为它占用内存少。包含一个名字和两个指向地址的指针的 Address 结构体可以使用 %v 打印:
{Kersschot 0x126d2b80 0x126d2be0}
~~~
**练习 10.2** persionext1.go:
修改 persionext1.go,使它的参数 upPerson 不是一个指针,解释下二者的区别。
**练习 10.3** point.go:
使用坐标 X、Y 定义一个二维 Point 结构体。同样地,对一个三维点使用它的极坐标定义一个 Polar 结构体。实现一个`Abs()` 方法来计算一个 Point 表示的向量的长度,实现一个 `Scale` 方法,它将点的坐标乘以一个尺度因子(提示:使用`math` 包里的 `Sqrt` 函数)(function Scale that multiplies the coordinates of a point with a scale factor)。
**练习 10.3** rectangle.go:
定义一个 Rectangle 结构体,它的长和宽是 int 类型,并定义方法 `Area()` 和 `Primeter()`,然后进行测试。
- 前言
- 第一部分:学习 Go 语言
- 第1章:Go 语言的起源,发展与普及
- 1.1 起源与发展
- 1.2 语言的主要特性与发展的环境和影响因素
- 第2章:安装与运行环境
- 2.1 平台与架构
- 2.2 Go 环境变量
- 2.3 在 Linux 上安装 Go
- 2.4 在 Mac OS X 上安装 Go
- 2.5 在 Windows 上安装 Go
- 2.6 安装目录清单
- 2.7 Go 运行时(runtime)
- 2.8 Go 解释器
- 第3章:编辑器、集成开发环境与其它工具
- 3.1 Go 开发环境的基本要求
- 3.2 编辑器和集成开发环境
- 3.3 调试器
- 3.4 构建并运行 Go 程序
- 3.5 格式化代码
- 3.6 生成代码文档
- 3.7 其它工具
- 3.8 Go 性能说明
- 3.9 与其它语言进行交互
- 第二部分:语言的核心结构与技术
- 第4章:基本结构和基本数据类型
- 4.1 文件名、关键字与标识符
- 4.2 Go 程序的基本结构和要素
- 4.3 常量
- 4.4 变量
- 4.5 基本类型和运算符
- 4.6 字符串
- 4.7 strings 和 strconv 包
- 4.8 时间和日期
- 4.9 指针
- 第5章:控制结构
- 5.1 if-else 结构
- 5.2 测试多返回值函数的错误
- 5.3 switch 结构
- 5.4 for 结构
- 5.5 Break 与 continue
- 5.6 标签与 goto
- 第6章:函数(function)
- 6.1 介绍
- 6.2 函数参数与返回值
- 6.3 传递变长参数
- 6.4 defer 和追踪
- 6.5 内置函数
- 6.6 递归函数
- 6.7 将函数作为参数
- 6.8 闭包
- 6.9 应用闭包:将函数作为返回值
- 6.10 使用闭包调试
- 6.11 计算函数执行时间
- 6.12 通过内存缓存来提升性能
- 第7章:数组与切片
- 7.1 声明和初始化
- 7.2 切片
- 7.3 For-range 结构
- 7.4 切片重组(reslice)
- 7.5 切片的复制与追加
- 7.6 字符串、数组和切片的应用
- 第8章:Map
- 8.1 声明、初始化和 make
- 8.2 测试键值对是否存在及删除元素
- 8.3 for-range 的配套用法
- 8.4 map 类型的切片
- 8.5 map 的排序
- 8.6 将 map 的键值对调
- 第9章:包(package)
- 9.1 标准库概述
- 9.2 regexp 包
- 9.3 锁和 sync 包
- 9.4 精密计算和 big 包
- 9.5 自定义包和可见性
- 9.6 为自定义包使用 godoc
- 9.7 使用 go install 安装自定义包
- 9.8 自定义包的目录结构、go install 和 go test
- 9.9 通过 Git 打包和安装
- 9.10 Go 的外部包和项目
- 9.11 在 Go 程序中使用外部库
- 第10章:结构(struct)与方法(method)
- 10.1 结构体定义
- 10.2 使用工厂方法创建结构体实例
- 10.3 使用自定义包中的结构体
- 10.4 带标签的结构体
- 10.5 匿名字段和内嵌结构体
- 10.6 方法
- 10.8 垃圾回收和 SetFinalizer
- 第11章:接口(interface)与反射(reflection)
- 11.1 接口是什么
- 11.2 接口嵌套接口
- 11.3 类型断言:如何检测和转换接口变量的类型
- 11.4 类型判断:type-switch
- 11.5 测试一个值是否实现了某个接口
- 11.6 使用方法集与接口
- 11.7 第一个例子:使用 Sorter 接口排序
- 11.8 第二个例子:读和写
- 11.9 空接口
- 11.10 反射包
- 第三部分:Go 高级编程
- 第12章 读写数据
- 12.1 读取用户的输入
- 12.2 文件读写
- 12.3 文件拷贝
- 12.4 从命令行读取参数
- 12.5 用buffer读取文件
- 12.6 用切片读写文件
- 12.7 用 defer 关闭文件
- 12.8 使用接口的实际例子:fmt.Fprintf
- 12.9 Json 数据格式
- 12.10 XML 数据格式
- 12.11 用 Gob 传输数据
- 12.12 Go 中的密码学
- 第13章 错误处理与测试
- 13.1 错误处理
- 13.2 运行时异常和 panic
- 13.3 从 panic 中恢复(Recover)
- 13.4 自定义包中的错误处理和 panicking
- 13.5 一种用闭包处理错误的模式
- 13.6 启动外部命令和程序
- 13.7 Go 中的单元测试和基准测试
- 13.8 测试的具体例子
- 13.9 用(测试数据)表驱动测试
- 13.10 性能调试:分析并优化 Go 程序
- 第14章:协程(goroutine)与通道(channel)
- 14.1 并发、并行和协程
- 14.2 使用通道进行协程间通信
- 14.3 协程同步:关闭通道-对阻塞的通道进行测试
- 14.4 使用 select 切换协程
- 14.5 通道,超时和计时器(Ticker)
- 14.6 协程和恢复(recover)
- 第15章:网络、模版与网页应用
- 15.1 tcp服务器
- 15.2 一个简单的web服务器
- 15.3 访问并读取页面数据
- 15.4 写一个简单的网页应用
- 第四部分:实际应用
- 第16章:常见的陷阱与错误
- 16.1 误用短声明导致变量覆盖
- 16.2 误用字符串
- 16.3 发生错误时使用defer关闭一个文件
- 16.5 不需要将一个指向切片的指针传递给函数
- 16.6 使用指针指向接口类型
- 16.7 使用值类型时误用指针
- 16.8 误用协程和通道
- 16.9 闭包和协程的使用
- 16.10 糟糕的错误处理
- 第17章:模式
- 17.1 关于逗号ok模式
- 第18章:出于性能考虑的实用代码片段
- 18.1 字符串
- 18.2 数组和切片
- 18.3 映射
- 18.4 结构体
- 18.5 接口
- 18.6 函数
- 18.7 文件
- 18.8 协程(goroutine)与通道(channel)
- 18.9 网络和网页应用
- 18.10 其他
- 18.11 出于性能考虑的最佳实践和建议
- 附录