Go语言学习笔记
参考(Ctrl c+v) https://www.kancloud.cn/kancloud/the-way-to-go/
当个学习笔记,只记录重点。
初始
平台与架构
Go 语言开发团队开发了适用于以下操作系统的编译器:
- Linux
- FreeBSD
- Mac OS X(也称为 Darwin)
目前有2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。其中,gc 版本的编译器已经被移植到 Windows 平台上,并集成在主要发行版中,你也可以通过安装 MinGW 从而在 Windows 平台下使用 gcc 编译器。这两个编译器都是以单通道的形式工作。
你可以获取以下平台上的 Go 1.4 源码和二进制文件:
- Linux 2.6+:amd64、386 和 arm 架构
- Mac OS X(Snow Leopard + Lion):amd64 和 386 架构
- Windows 2000+:amd64 和 386 架构
Go 环境变量
- $GOROOT 表示 Go 在你的电脑上的安装位置,它的值一般都是
$HOME/go
,当然,你也可以安装在别的地方。 - $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。
- $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
- $GOBIN 表示编译器和链接器的安装位置,默认是
$GOROOT/bin
,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。
安装目录清单
/bin
:包含可执行文件,如:编译器,Go 工具/doc
:包含示例程序,代码工具,本地文档等/lib
:包含文档模版/misc
:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例/os_arch
:包含标准库的包的对象文件(.a
)/src
:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)/src/cmd
:包含 Go 和 C 的编译器和命令行脚本
Go调试器
- 在合适的位置使用打印语句输出相关变量的值(
print
/println
和fmt.Print
/fmt.Println
/fmt.Printf
)。 - 在
fmt.Printf
中使用下面的说明符来打印有关变量的相关信息:%+v
打印包括字段在内的实例的完整信息%#v
打印包括字段和限定类型名称在内的实例的完整信息%T
打印某个类型的完整说明
- 使用 panic 语句(第 13.2 节)来获取栈跟踪信息(直到 panic 时所有被调用函数的列表)。
- 使用关键字 defer 来跟踪代码执行过程(第 6.4 节)。
构建并运行 Go 程序
在大多数 IDE 中,每次构建程序之前都会自动调用源码格式化工具 gofmt
并保存格式化后的源文件。如果构建成功则不会输出任何信息,而当发生编译时错误时,则会指明源码中具体第几行出现了什么错误,如:a declared and not used
。一般情况下,你可以双击 IDE 中的错误信息直接跳转到发生错误的那一行。
如果程序执行一切顺利并成功退出后,将会在控制台输出 Program exited with code 0
。
从 Go 1 版本开始,使用 Go 自带的更加方便的工具来构建应用程序:
go build
编译并安装自身包和依赖包go install
安装自身包和依赖包
语言的核心结构与技术
基本结构和基本数据类型
hello world
1 | package main |
包的概念
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main
的包。
所有的包名都应该使用小写字母。
注释
//
单行注释
/* xxxx */
多行注释
函数
你可以在括号 ()
中写入 0 个或多个函数的参数(使用逗号 ,
分隔),每个参数的名称后面必须紧跟着该参数的类型。
1 | func Sum(a, b int) int { return a + b } |
类型
基本类型:int
、float
、bool
、string
结构化的(复合的):struct
、array
、slice
、map
、channel
结构化的类型没有真正的值,它使用nil
作为默认值
类型转换
类型 B 的值 = 类型 B(类型 A 的值)
1 | valueOfTypeB = typeB(valueOfTypeA) |
常量
1 | const beef, two, c = “meat”, 2, “veg” |
变量
声明变量时将变量的 类型 放在变量的 名称之后
1 | var identifier type |
变量的 命名规则 遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips
和 startDate
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。
如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包、外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
打印
函数 fmt.Print
和 fmt.Println
会自动使用格式化标识符 %v
对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:
1 | fmt.Print("Hello:", 23) |
init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。
不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
一个源文件都可以包含且只包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
init.go
1 | package trans |
user_init.go 中导入了包 trans(在相同的路径中)并且使用到了变量 Pi
1 | package main |
基本类型和运算符
布尔类型 bool:布尔型的值只可以是常量 true 或者 false
数字类型:整型 int 和浮点型 float
格式化说明符:格式化字符串里,%d
用于格式化整数(%x
和 %X
用于格式化 16 进制表示的数字),%g
用于格式化浮点型(%f
输出浮点数,%e
输出科学计数表示法),%0d
用于规定输出定长的整数,其中开头的数字 0 是必须的。
数字值转换:进行a32bitInt = int32(a32Float)
的转换时,小数点后的数字将被丢弃。
复数:Go 拥有以下复数类型:
1 | complex64 (32 位实数和虚数) |
位运算:
一元运算符:按位补足 ^
,位左移 <<
,位右移 >>
二元运算符:按位与 &
,按位或 |
,按位异或 ^
,位清除 &^
逻辑运算符:==
, !=
, <
, <=
, >
, >=
算术运算符:常见可用于整数和浮点数的二元运算符有 +
、-
、*
和 /
。
运算符与优先级:
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
1 | 优先级 运算符 |
当然,你可以通过使用括号来临时提升某个表达式的整体运算优先级。
类型别名:
1 | type TZ int |
字符类型:char.go
1 | var ch int = '\u0041' |
字符串
string
类型的零值为长度为零的字符串,即空字符串 ""
。
一般的比较运算符(==
、!=
、<
、<=
、>=
、>
)通过在内存中按字节比较来实现字符串的对比。你可以通过函数len()
来获取字符串所占的字节长度,例如:len(str)
。
字符串拼接符 +
:两个字符串 s1
和 s2
可以通过 s := s1 + s2
、s += "world"
拼接在一起。
解释字符串:
该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:
\n
:换行符\r
:回车符\t
:tab 键\u
或\U
:Unicode 字符\\
:反斜杠自身
非解释字符串:
该类字符串使用反引号括起来,支持换行,例如:
1
`This is a raw string \n` 中的 `\n\` 会被原样输出。
时间和日期
1 | package main |
指针
程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b0820
或0xf84001d7f0
。
一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。
Go 语言的取地址符是 &
,放到一个变量前使用就会返回相应变量的内存地址。
指针类型前面加上*
号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil
。
一个指针变量通常缩写为 ptr
。
例:展示了分配一个新的值给 *p 并且更改这个变量自己的值(这里是一个字符串)
1 | package main |
控制结构
if-else 结构
1 | if condition1 { |
switch 结构
1 | switch { |
for结构
基于计数器的迭代
1 | package main |
for-range 结构
它可以迭代任何一个集合(包括数组和 map),一般形式为:for ix, val := range coll { }
。
val
始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(译者注:如果 val
为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)
1 | for pos, char := range str { |
Break 与 continue
break
的作用范围为该语句出现后的最内部的结构,它可以被用于任何形式的 for 循环(计数器、条件判断等)。但在 switch 或 select 语句中,break 语句的作用结果是跳过整个代码块,执行后续的代码。
continue
忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。
标签与 goto
for
、switch
或 select
语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:
)结尾的单词(gofmt 会将后续代码自动移至下一行)。
1 | LABEL1: |
函数(function)
函数参数与返回值
按值\引用传递
1 | package main |
命名的返回值
getX2AndX3
与 getX2AndX3_2
两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 ()
把它们括起来,比如 (int, int)
。
1 | package main |
空白符
空白符用来匹配一些不需要的值,然后丢弃掉。ThreeValues
是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1
与f1
。第二个返回值赋给了空白符 _
,然后自动丢弃掉。
1 | package main |
改变外部变量
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return
返回。如下的例子,reply
是一个指向 int
变量的指针,通过这个指针,我们在函数内修改了这个 int
变量的数值。
1 | package main |
传递变长参数
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
示例函数和调用:
1 | func Greeting(prefix string, who ...string) |
在 Greeting 函数中,变量 who
的值为 []string{"Joe", "Anna", "Eileen"}
。
如果参数被存储在一个数组 arr
中,则可以通过 arr...
的形式来传递参数调用变参函数。
1 | package main |
defer 和追踪
https://www.kancloud.cn/kancloud/the-way-to-go/72476
关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return
语句同样可以包含一些操作,而不是单纯地返回某个值)。
关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally
语句块,它一般用于释放某些已分配的资源。
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
可以使用 defer 语句实现代码追踪
内置函数
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用户内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它的括号 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数(详见第 4.2 节),在部署环境中建议使用 fmt 包 |
complex、real imag | 用于创建和操作复数(详见第 4.5.2.2 节 |
递归函数
当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即每个数均为前两个数之和。数列如下所示:
1 | 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, … |
1 | package main |
将函数作为参数
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调 callback
。
1 | package main |
闭包
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }
。
这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body
),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y }
,然后通过变量名对函数进行调用:fplus(3,4)
。
当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)
。
应用闭包:将函数作为返回值
https://www.kancloud.cn/kancloud/the-way-to-go/72481
1 | package main |
使用闭包调试
能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime
或 log
包中的特殊函数来实现这样的功能。包runtime
中的函数 Caller()
提供了相应的信息,因此可以在需要的时候实现一个 where()
闭包函数来打印函数执行的位置:
1 | where := func() { |
您也可以设置 log
包中的 flag 参数来实现:
1 | log.SetFlags(log.Llongfile) |
或使用一个更加简短版本的 where
函数:
1 | var where = log.Print |
计算函数执行时间
在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time
包中的 Now()
和 Sub
函数:
1 | start := time.Now() |
通过内存缓存来提升性能
进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。最明显的例子就是生成斐波那契数列的程序
1 | package main |
数组与切片
声明和初始化
数组
数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。
声明的格式是:
1 | var identifier [len]type |
切片
优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。
声明切片的格式是: var identifier []type
(不需要说明长度)。
一个切片在未初始化之前默认为 nil,长度为 0。
切片的初始化格式是:var slice1 []type = arr1[start:end]
这表示 slice1 是由数组 arr1 从 start 索引到 end-1
索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0]
就等于 arr1[start]
。
For-range 结构
这种构建方法可以应用与数组和切片:
1 | for ix, value := range slice1 { |
假设我们有如下数组:items := [...]int{10, 20, 30, 40, 50}
1 | for _, item := range items { |
切片重组(reslice)
切片可以反复扩展直到占据整个相关数组。
示例 7.11 reslicing.go
1 | package main |
切片的复制与追加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。
1 | package main |
字符串、数组和切片的应用
7.6.2 获取字符串的某一部分
7.6.3 字符串和切片的内存结构
7.6.4 修改字符串中的某个字符
7.6.5 字节数组对比函数
Compare
函数会返回两个字节数组字典顺序的整数对比结果7.6.6 搜索及排序切片和数组
sort
包来实现常见的搜索和排序操作。7.6.7 append 函数常见操作
我们在第 7.5 节提到的 append 非常有用,它能够用于各种方面的操作:
将切片 b 的元素追加到切片 a 之后:
a = append(a, b...)
复制切片 a 的元素到新的切片 b 上:
1
2b = make([]T, len(a))
copy(b, a)删除位于索引 i 的元素:
a = append(a[:i], a[i+1:]...)
切除切片 a 中从索引 i 至 j 位置的元素:
a = append(a[:i], a[j:]...)
为切片 a 扩展 j 个元素长度:
a = append(a, make([]T, j)...)
在索引 i 的位置插入元素 x:
a = append(a[:i], append([]T{x}, a[i:]...)...)
在索引 i 的位置插入长度为 j 的新切片:
a = append(a[:i], append(make([]T, j), a[i:]...)...)
在索引 i 的位置插入切片 b 的所有元素:
a = append(a[:i], append(b, a[i:]...)...)
取出位于切片 a 最末尾的元素 x:
x, a = a[len(a)-1], a[:len(a)-1]
将元素 x 追加到切片 a:
a = append(a, x)
7.6.8 切片和垃圾回收
Map
map 是一种特殊的数据结构:一种元素对(pair)的无序集合,pair 的一个元素是 key,对应的另一个元素是 value,所以这个结构也称为关联数组或字典。这是一种快速寻找值的理想结构:给定 key,对应的 value 可以迅速定位。
map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和 HashTable 等。
声明、初始化和 make
map 是引用类型,可以使用如下声明:
1 | var map1 map[keytype]valuetype |
1 | package main |
测试键值对是否存在及删除元素
如果你只是想判断某个 key 是否存在而不关心它对应的值到底是多少,你可以这么做:
1 | _, ok := map1[key1] // 如果key1存在则ok == true,否在ok为false |
或者和 if 混合使用:
1 | if _, ok := map1[key1]; ok { |
for-range 的配套用法
可以使用 for 循环构造 map:
1 | for key, value := range map1 { |
第一个返回值 key 是 map 中的 key 值,第二个返回值则是该 key 对应的 value 值;这两个都是仅 for 循环内部可见的局部变量。其中第一个返回值key值是一个可选元素。如果你只关心值,可以这么使用:
1 | for _, value := range map1 { |
如果只想获取 key,你可以这么使用:
1 | for key := range map1 { |
map 类型的切片
假设我们想获取一个 map 类型的切片,我们必须使用两次 make()
函数,第一次分配切片,第二次分配 切片中每个 map 元素
1 | package main |
输出结果:
1 | Version A: Value of items: [map[1:2] map[1:2] map[1:2] map[1:2] map[1:2]] |
需要注意的是,应当像 A 版本那样通过索引使用切片的 map 元素。在 B 版本中获得的项只是 map 值的一个拷贝而已,所以真正的 map 元素没有得到初始化。
map 的排序
map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序
如果你想为 map 排序,需要将 key(或者 value)拷贝到一个切片,再对切片排序(使用 sort 包,详见第 7.6.6 节),然后可以使用切片的 for-range 方法打印出所有的 key 和 value。
将 map 的键值对调
这里倒置是指调换 key 和 value。如果 map 的值类型可以作为 key 且所有的 value 是唯一的,那么通过下面的方法可以简单的做到键值对调。
1 | package main |
包(package)
标准库概述
像 fmt
、os
等这样具有常用功能的内置包在 Go 语言中有 150 个以上,它们被称为标准库,大部分(一些底层的除外)内置于 Go 本身。完整列表可以在 Go Walker 查看。
unsafe
: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。syscall
-os
-os/exec
:os
: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。os/exec
: 提供我们运行外部操作系统命令和程序的方式。syscall
: 底层的外部包,提供了操作系统底层调用的基本接口。archive/tar
和/zip-compress
:压缩(解压缩)文件功能。fmt
-io
-bufio
-path/filepath
-flag
:fmt
: 提供了格式化输入输出功能。io
: 提供了基本输入输出功能,大多数是围绕系统功能的封装。bufio
: 缓冲输入输出功能的封装。path/filepath
: 用来操作在当前系统中的目标文件名路径。flag
: 对命令行参数的操作。
strings
-strconv
-unicode
-regexp
-bytes
:strings
: 提供对字符串的操作。strconv
: 提供将字符串转换为基础类型的功能。unicode
: 为 unicode 型的字符串提供特殊的功能。regexp
: 正则表达式功能。bytes
: 提供对字符型分片的操作。index/suffixarray
: 子字符串快速查询。
math
-math/cmath
-math/big
-math/rand
-sort
:math
: 基本的数学函数。math/cmath
: 对复数的操作。math/rand
: 伪随机数生成。sort
: 为数组排序和自定义集合。math/big
: 大数的实现和计算。
container
-/list-ring-heap
: 实现对集合的操作。list
: 双链表。
time
-log
:time
: 日期和时间的基本操作。log
: 记录程序运行时产生的日志,我们将在后面的章节使用它。
encoding/json
-encoding/xml
-text/template
:encoding/json
: 读取并解码和写入并编码 JSON 数据。encoding/xml
:简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。text/template
:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
net
-net/http
-html
:(参见第 15 章)net
: 网络数据的基本操作。http
: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。html
: HTML5 解析器。
runtime
: Go 程序运行时的交互操作,例如垃圾回收和协程创建。reflect
: 实现通过程序运行时反射,让程序操作任意类型的变量。
regexp 包
下面的程序里,我们将在字符串中对正则表达式进行匹配。
如果是简单模式,使用 Match
方法便可:
1 | ok, _ := regexp.Match(pat, []byte(searchIn)) |
变量 ok 将返回 true 或者 false,我们也可以使用 MatchString
:
1 | ok, _ := regexp.MathString(pat, searchIn) |
更多方法中,必须先将正则通过 Compile
方法返回一个 Regexp 对象。然后我们将掌握一些匹配,查找,替换相关的功能。
示例 9.2 pattern.go:
1 | package main |
锁和 sync 包
精密计算和 big 包
自定义包和可见性
为自定义包使用 godoc
使用 go install 安装自定义包
自定义包的目录结构、go install 和 go test
通过 Git 打包和安装
Go 的外部包和项目
结构(struct)与方法(method)
结构体定义(struct)
结构体定义的一般方式如下:
1 | type identifier struct { |
type T struct {a, b int}
也是合法的语法,它更适用于简单的结构体。
https://www.kancloud.cn/kancloud/the-way-to-go/72512
使用工厂方法创建结构体实例
Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂“ 方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型:
1 | type File struct { |
下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:
1 | func NewFile(fd int, name string) *File { |
然后这样调用它:
1 | f := NewFile(10, "./test.txt") |
在 Go 语言中常常像上面这样在工厂方法里使用初始化来简便的实现构造子。
如果 File
是一个结构体类型,那么表达式 new(File)
和 &File{}
是等价的。
这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:File f = new File(...)
。
我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样。
如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})
。
使用自定义包中的结构体
下面的例子中,main.go 使用了一个结构体,它来自 struct_pack 下的包 structPack。
示例 10.5 structPack.go:
1 | package structPack |
示例 10.6 main.go:
1 | package main |
带标签的结构体
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect
能获取它。我们将在下一章(第 11.10 节)中深入的探讨 reflect
包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf()
可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
示例 10.7 struct_tag.go:
1 | package main |
匿名字段和内嵌结构体
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
匿名结构体
1 | package main |
通过类型 outer.int
的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。
内嵌结构体
同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。外层结构体通过outer.in1
直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。
1 | package main |