Golang 基础语法
1 - 包
在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能,更好地复用代码,并对每个包内的数据的使用有更好的控制。
包的规则
- 所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。
- 每个包都在一个单独的目录里。
- 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。
- 这意味着,同一个目录下的所有.go 文件必须声明同一个包名。
包的命名
- 给包命名的惯例是使用包所在目录的名字
- 给包及其目录命名时,应该使用简洁、清晰且全小写的名字
记住:并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。
main包
在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。
包的导入
导入包需要使用关键字import ,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中。
import (
"fmt"
"strings"
)
编译器查找packge的顺序:
- 标准库,如
/usr/local/go/src/*/*
,这个路径由环境变量GOROOT决定。 - 当前项目源代码路径,如
/home/myproject/src/*/*
- gopath路径,如
/home/sky/work/soft/go/*/*
,这个路径由环境变量GOPATH决定。
查找策略:一旦编译器找到一个满足 import 语句的包,就停止进一步查找。
远程导入
Go 支持从远程网站获取源代码,如github:
import "github.com/spf13/viper"
获取过程使用 go get
命令完成
命名导入
重名的包可以通过命名导入来导入。
命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
import (
"fmt"
myfmt "mylib/fmt"
)
导入一个不在代码里使用的包时,Go 编译器会编译失败,输出错误。这个特性可以防止导入了未被使用的包,避免代码变得臃肿。
如果需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_
来重命名这个导入:
import (
_ "mylib/fmt"
)
init函数
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。
多个init函数之间的执行顺序不定,不要依赖这个顺序。
所有被编译器发现的 init 函数都会安排在 main 函数之前执行。
init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
2 - 常量
常量的定义
常量的定义与变量类似,只不过使用 const
关键字。
const Pi = 3.14
func main() {
const World = "world"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")
const Truth = true
fmt.Println("Go rules?", Truth)
}
常量可以是字符、字符串、布尔或数字类型的值。
注意:常量不能使用 :=
语法定义。
数值常量
数值常量是高精度的 值。
const (
Big = 1 << 100
Small = Big >> 99
)
一个未指定类型的常量由上下文来决定其类型。
go语言规范
https://golang.org/ref/spec#Constants
https://moego.me/golang_spec.html#id271
常量有 布尔值常量 、 rune 常量 、 整数常量 、 浮点数常量 、 复数常量 和 字符串常量 。 Rune、整数、浮点数和复数常量统称为数值常量。
常量的值是由如下所表示的: rune,整数,浮点数,虚数,字符串字面值,表示常量的标识符,常量表达式,结果为常量的变量转换,或者一些内置函数所生成的值,这些内置函数比如应用于任意值的 unsafe.Sizeof ,应用于一些表达式 的 cap 或 len ,应用于复数常量的 real 和 imag 以及应用于数值常量的 complex 。布尔值是由预先声明的常量 true 和 false 所代表的。预先声明的标识符 iota 表示一个整数常量。
通常,复数常量是 常量表达式 的一种形式,会在该节讨论。
数值常量代表任意精度的确切值,而且不会溢出。因此,没有常量表示 IEEE-754 负零,无穷,以及非数字值集。
常量可以是有类型的也可以是无类型的。字面值常量, true , false , iota 以及一些仅包含无类型的恒定操作数的 常量表达式 是无类型的。
常量可以通过 常量声明 或 变量转换 被显示地赋予一个类型,也可以在 变量声明 或 赋值 中,或作为一个操作数在 表达式 中使用时隐式地被赋予一个类型。如果常量的值不能按照所对应的类型来表示的话,就会出错。「前一版的内容: 比如, 3.0 可以作为任何整数类型或任何浮点数类型,而 2147483648.0 (相当于 1«31 )可以作为 float32 , float64 或 uint32 类型,但不能是 int32 或 string 。」
一个无类型的常量有一个 默认类型 ,当在上下文中需要请求该常量为一个带类型的值时,这个 默认类型 便指向该常量隐式转换后的类型,比如像 i := 0 这样子的 短变量声明 就没有显示的类型。无类型常量的默认类型分别是 bool , rune , int , float64 , complex128 或 string ,取决于它是否是一个布尔值、 rune、整数、浮点数、复数或字符串常量。
实现限制:虽然数值常量在这个语言中可以是任意精度的,但编译器可能会使用精度受限的内部表示法来实现它。也就是说,每一种实现必须:
- 使用最少 256 位来表示整数。
- 使用最少 256 位来表示浮点数常量(包括复数常量的对应部分)的小数部分,使用最少 16 位表示其带符号的二进制指数部分。
- 当无法表示一个整数常量的精度时,需要给出错误。
- 当因为溢出而无法表示一个浮点数或复数常量时,需要给出错误。
- 当因为精度限制而无法表示一个浮点数或复数常量时,约到最接近的可表示的常量。
这些要求也适用于字面值常量,以及 常量表达式 的求值结果。
Effective Go
https://golang.org/doc/effective_go.html#constants
Go中的常量仅仅是常量。它们是在编译时创建的,即使是在函数中定义为locals,也只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制,定义它们的表达式必须是常量表达式,可被编译器评估。例如,1«3是一个常量表达式,而math.Sin(math.Pi/4)不是,因为对math.Sin的函数调用需要在运行时发生。
在Go中,使用iota枚举器创建枚举常量。由于 iota 可以成为表达式的一部分,而且表达式可以隐式重复,因此很容易建立复杂的值集。
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
将String这样的方法附加到任何用户定义的类型上的能力,使得任意值在打印时自动格式化成为可能。虽然你最常看到的是它应用于结构体,但这种技术对于标量类型也很有用,比如浮点类型(如ByteSize)。
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
表达式YB打印为1.00YB,而ByteSize(1e13)打印为9.09TB。
这里使用Sprintf实现ByteSize的String方法是安全的(避免无限期重复),不是因为转换,而是因为它用%f调用Sprintf,而%f不是字符串格式。Sprintf只有在需要字符串时才会调用String方法,而%f需要的是浮点值。
3 - 变量
变量定义
var
语句定义一个变量的列表;跟函数的参数列表一样,类型在后面。
var
语句可以定义在包或函数级别。
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
变量初始化
// 变量定义可以包含初始值,每个变量对应一个。
var i, j int = 1, 2
func main() {
// 如果初始化是使用表达式,则可以省略类型;
// 变量从初始值中获得类型。
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
也可以用这个方式初始化多个变量,每个变量一行代码:
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
如果变量在定义时没有明确的初始化,则会赋值为该类型对应的零值:
- 数值类型为
0
, - 布尔类型为
false
, - 字符串为
""
(空字符串)
短声明变量
在函数中,:=
简洁赋值语句在明确类型的地方,可以用于替代 var
定义。
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
}
函数外的每个语句都必须以关键字开始(var
、func
、等等),:=
结构不能使用在函数外。
多赋值模式
上面同时赋值多个变量的模式,可以用于if, for 等语句,实现在if, for 语句中的多赋值模式:
for i, j, s := 0, 5, "a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a" {
fmt.Println("Value of i, j, s:", i, j, s)
}
go语言规范
https://golang.org/ref/spec#Variables
变量是用来存放数值的存储位置。允许的值集由变量的类型决定。
变量声明,或者对于函数参数和结果,函数声明或函数字面量的签名为命名的变量保留了存储空间。调用内置函数new或取复合字面的地址,在运行时为变量分配存储空间。这样的匿名变量是通过(可能是隐含的)指针间接引用的。
数组、切片和结构体类型的结构化变量具有可以单独寻址的元素和字段。每个这样的元素都像一个变量一样。
变量的静态类型(或者说只是类型)是在它的声明中给出的类型,新调用或复合文字中提供的类型,或者是结构变量的元素的类型。接口类型的变量也有一个独特的动态类型,它是在运行时分配给变量的值的具体类型(除非该值是预先声明的标识符nil,它没有类型)。动态类型在执行过程中可能会发生变化,但存储在接口变量中的值总是可以分配给变量的静态类型。
var x interface{} // x 是 nil,它有一个静态类型 interface{}
var v *T // v 的值为 nil,静态类型为 *T
x = 42 // x 的值为 42,动态类型为 int
x = v // x 的值为 (*T)(nil),动态类型为 *T
变量的值通过在表达式中引用变量来检索;它是分配给变量的最新值。如果一个变量还没有被赋值,它的值就是它的类型的零值。
Effective Go
https://golang.org/doc/effective_go.html#variables
变量可以像常量一样被初始化,但初始化器可以是一个在运行时计算的通用表达式。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
4 - 指针
Go 具有指针。 指针保存了变量的内存地址。
基本语法
定义:类型 *T
是指向类型 T
的值的指针。其零值是 nil
。
var p *int
生成:&
符号会生成一个指向其作用对象的指针。
i := 42
p = &i
取值:*
符号表示指针指向的底层的值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
注意:与 C 不同,Go 没有指针运算。
5 - 函数
语法
Golang的函数语法和Java、C等有非常大的不同:
- 函数参数定义是类型在变量名之后,而不是变量名之前
- 函数返回值定义在函数签名之后,而不是函数签名之前
- 函数可以返回多个值
func swap(x, y string) (string, string) {
return y, x
}
参数
函数可以没有参数或有多个参数。
注意类型在变量名 之后。
func add(x int, y int) int {
return x + y
}
当两个或多个连续的参数是同一类型,则除了最后一个类型之外,其他都可以省略。
func add(x, y int) int {
return x + y
}
返回值
函数可以返回任意数量的返回值。这点和很多语言不同:
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
Go 的返回值可以被命名,并且像变量那样使用。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
6 - Getter
备注:摘录自 Effective Go https://golang.org/doc/effective_go.html#Getters
Getters
Go并没有提供对getter和setter的自动支持。自己提供getter和setter并没有错,而且这样做通常是合适的,但在getter的名称中加上Get既不习惯也没有必要。如果你有一个叫做 owner 的字段(小写,未导出),那么 getter 方法应该叫做 Owner(大写,导出),而不是 GetOwner。导出时使用大写的名称提供了区分字段和方法的钩子。如果需要的话,setter函数可能会被称为SetOwner。这两个名字在实践中都很好读。
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
7 - slice
Go Slices: usage and internals
https://blog.golang.org/slices-intro
介绍
Go 的 slice 类型为处理类型化数据序列提供了一种方便而有效的方法。分片类似于其他语言中的数组,但有一些不同寻常的特性。本文将介绍什么是切片以及如何使用它们。
数组
分片类型是建立在Go的数组类型之上的一种抽象,所以要理解分片,我们必须先理解数组。
数组类型定义指定了长度和元素类型。例如,类型 [4]int 表示一个由四个整数组成的数组。数组的大小是固定的,它的长度是其类型的一部分([4]int 和 [5]int 是不同的、不兼容的类型)。数组可以用通常的方式进行索引,所以表达式 s[n] 是访问从零开始的第n个元素。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
数组不需要显式初始化,数组的零值是一个可以直接使用的数组,其元素本身是置零的:
// a[2] == 0, the zero value of the int type
[4]int的内存表示是四个整数值线性排列:
Go的数组是数值。一个数组变量表示整个数组;它不是指向第一个数组元素的指针(在 C 语言中是这样)。这意味着,当您分配或传递一个数组值时,您将复制它的内容。(为了避免复制,你可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组)。有一种方法可以把数组看作是一种结构,但它是有索引而不是命名字段:一个固定大小的复合值。
可以像这样指定一个数组文字:
b := [2]string{"Penn", "Teller"}
或者,你可以让编译器为你计算数组元素:
b := [...]string{"Penn", "Teller"}
在这两种情况下,b的类型都是 [2]string 。
Slices
数组有它们的位置,但它们有点不灵活,所以你在 Go 代码中不太常见。不过,Slices却无处不在。它们建立在数组的基础上,提供了强大的功能和便利。
分片的类型规范是 []T,其中T是分片元素的类型。与数组类型不同,分片类型没有指定的长度。
分片文字的声明就像数组文字一样,只是省略了元素数:
letters := []string{"a", "b", "c", "d"}
可以用内置函数make创建切片,它的签名是:
func make([]T, len, cap) []T
其中T代表要创建的分片的元素类型。make函数需要类型、长度和可选的容量。调用时,make会分配一个数组并返回一个指向该数组的分片:
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
当省略capacity参数时,它默认为指定的长度。下面是相同代码的一个更简洁的版本:
s := make([]byte, 5)
使用内置的 length 和 capacity 函数可以检查切片的长度和容量:
len(s) == 5
cap(s) == 5
接下来的两节将讨论长度和容量之间的关系。
分片的零值是nil。len 和cap 函数都会对nul分片返回0。
分片也可以通过 “切片” 现有的分片或数组来形成。切片是通过指定一个半开放的范围,用冒号隔开两个指数来完成的。例如,表达式 b[1:4] 创建了一个包含b元素1到3的分片(生成的分片的指数为0到2)。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
分片表达式的 start 和 end 指数是可选的,它们分别默认为零和分片的长度:
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
这是创建一个给定数组的分片的语法:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
切片内部
分片是数组段的描述符,它由指向数组的指针、段的长度和它的容量(段的最大长度)组成。
我们前面通过 make([]byte,5) 创建的变量s,结构是这样的:
长度是指分片指针所指的元素数。容量是底层数组中的元素数(从分片指针所指的元素开始)。在接下来的几个例子中,长度和容量之间的区别将变得很清楚。
当我们分片时,观察分片数据结构的变化以及它们与底层数组的关系:
s = s[2:4]
分片并不复制分片的数据,而是创建一个指向原始数组的新分片值。它创建一个新的分片值,指向原始数组。这使得分片操作与操作数组索引一样高效。因此,修改重新分片的元素(不是分片本身)会修改原始分片的元素:
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
刚才我们把s切成了比它的容量短的长度。我们可以通过再次将s切成片来增长它的容量:
s = s[:cap(s)]
分片的增长不能超过其容量。试图这样做会引起运行时的恐慌(panic),就像在分片或数组的边界之外索引一样。同样,不能将分片重新分割到零以下以访问数组中的早期元素。
增长切片(copy和append函数)
要增加一个分片的容量,必须创建一个新的、更大的分片,并将原来分片的内容复制到其中。这种技术是其他语言的动态数组在幕后实现的工作方式。下一个例子通过创建一个新的分片t,将s的内容复制到t中,然后将分片值t赋给s,从而使s的容量增加一倍。
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
这个常用操作的循环部分,通过内置的copy函数变得更加简单。顾名思义,copy将数据从一个源片复制到一个目标片。它返回复制的元素数量:
func copy(dst, src []T) int
copy函数支持在不同长度的切片之间进行复制(它将只复制到较小数量的元素)。此外,copy还可以处理共享同一个底层数组的源片和目的片,正确处理重叠的分片。
使用copy,我们可以简化上面的代码片段:
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一个常见的操作是将数据追加到一个分片的末尾。这个函数将字节元素追加到一个字节分片上,如果需要的话,会增加分片,并返回更新后的分片值:
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
可以这样使用AppendByte:
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
像AppendByte这样的函数很有用,因为它们提供了对分片生长方式的完全控制。根据程序的特性,可能需要以较小或较大的分片进行分配,或者对重新分配的大小设置一个上限。
但大多数程序不需要完全控制,所以Go提供了一个内置的append函数,对大多数目的都很好,它的签名是:
func append(s []T, x ...T) []T
append函数将元素x追加到分片s的末尾,如果需要更大的容量,则增长分片:
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
要将一个分片附加到另一个分片上,请使用…将第二个参数扩展为一个参数列表:
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
由于分片的零值(nil)就像一个零长度的分片,所以你可以声明一个分片变量,然后在循环中追加到它:
// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
一个可能的 “疑难杂症”
如前所述,重新切割一个分片并不会对底层数组进行复制。完整的数组将被保留在内存中,直到它不再被引用。偶尔,这可能会导致程序在只需要一小块数据的时候,将所有数据保存在内存中。
例如,这个FindDigits函数将一个文件加载到内存中,并在内存中搜索第一组连续的数字数字,将它们作为一个新的分片返回。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
这段代码的行为很明确,但是返回的 []字节 指向一个包含整个文件的数组。由于分片引用了原来的数组,所以只要分片被保留在周围,垃圾收集器就不能释放数组;文件的几个有用字节将整个内容保留在内存中。
为了解决这个问题,可以在返回之前将感兴趣的数据复制到一个新的分片中:
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
这个函数的一个更简洁的版本可以通过使用append来构建。这是留给读者的一个练习。
进一步阅读
Effective Go包含了对切片和数组的深入处理,Go语言规范定义了切片及其相关的帮助函数。
Slides in Effective Go
https://golang.org/doc/effective_go.html#slices
https://bingohuang.gitbooks.io/effective-go-zh-en/content/08_Data.html
切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。 除了矩阵变换这类需要明确维度的情况外,Go 中的大部分数组编程都是通过切片来完成的。
切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。
因此,Read 函数可接受一个切片实参 而非一个指针和一个计数;切片的长度决定了可读取数据的上限。以下为 os 包中 File 类型的 Read 方法签名:
func (file *File) Read(buf []byte) (n int, err error)
该方法返回读取的字节数和一个错误值(若有的话)。若要从更大的缓冲区 b 中读取前 32 个字节,只需对其进行切片即可:
n, err := f.Read(buf[0:32])
这种切片的方法常用且高效。若不谈效率,以下片段同样能读取该缓冲区的前 32 个字节:
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。 切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。 以下是将数据追加到切片的函数。若数据超出其容量,则会重新分配该切片。返回值即为所得的切片。 该函数中所使用的 len 和 cap 在应用于 nil 切片时是合法的,它会返回 0.
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
最终我们必须返回切片,因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。
向切片追加东西的想法非常有用,因此有专门的内建函数 append。 要理解该函数的设计,我们还需要一些额外的信息,我们将稍后再介绍它。
二维切片
Go 的数组和切片都是一维的。要创建等价的二维数组或切片,就必须定义一个数组的数组, 或切片的切片,就像这样:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片。在我们的 LinesOfText 例子中,这是种常见的情况:每行都有其自己的长度。
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
有时必须分配一个二维数组,例如在处理像素的扫描行时,这种情况就会发生。 我们有两种方式来达到这个目的。一种就是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它。采用哪种方式取决于你的应用。若切片会增长或收缩, 就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。 以下是这两种方法的大概代码,仅供参考。首先是一次一行的:
// 分配顶层切片。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 遍历行,为每一行都分配切片
for i := range picture {
picture[i] = make([]uint8, XSize)
}
现在是一次分配,对行进行切片:
// 分配顶层切片,和前面一样。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 分配一个大的切片来保存所有像素
pixels := make([]uint8, XSize*YSize) // 拥有类型 []uint8,尽管图片是 [][]uint8.
// 遍历行,从剩余像素切片的前面切出每行来。
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Go语言规范中的Slice
https://golang.org/ref/spec#Slice_types
分片是针对一个底层数组的连续段的描述符,它提供了对该数组内有序序列元素的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量被称为分片长度,且不能为负。未初始化的分片的值为 nil
。
SliceType = "[", "]", ElementType .
分片 s
的长度可以被内置函数 len来发现;和数组不同的是,这个长度可能会在执行过程中改变。元素可以被从 0
索引到 len(s) - 1
的整数所寻址到。一个给定元素的分片索引可能比其底层数组的相同元素的索引要小。
分片一旦初始化便始终关联到存放其元素的底层数组。因此分片会与其数组和其它相同数组的分片共享存储区;相比之下,不同的数组总是代表不同的存储区域。
分片底层的数组可以延伸超过分片的末端。 容量 便是对这个范围的测量:它是分片长度和数组内除了该分片以外的长度的和;不大于其容量长度的分片可以从原始分片再分片新的来创建。分片 a
的容量可以使用内置函数 cap(a) 来找到。
对于给定元素类型 T
的新的初始化好的分片值的创建是使用的内置函数 make,它需要获取分片类型、指定的长度和可选的容量作为参数。使用 make
创建的分片总是分配一个新的隐藏的数组给返回的分片值去引用。也就是,执行
make([]T, length, capacity)
就像分配个数组然后再分片它一样来产生相同的分片,所以如下两个表达式是相等的:
make([]int, 50, 100)
new([100]int)[0:50]
如同数组一样,分片总是一维的但可以通过组合来构造高维的对象。数组间组合时,被构造的内部数组总是拥有相同的长度;但分片与分片(或数组与分片)组合时,内部的长度可能是动态变化的。此外,内部分片必须单独初始化。