这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

Golang 基础语法

Golang 的基础语法

1 - 包

Golang packge

在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能,更好地复用代码,并对每个包内的数据的使用有更好的控制。

包的规则

  • 所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。
  • 每个包都在一个单独的目录里。
    • 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。
    • 这意味着,同一个目录下的所有.go 文件必须声明同一个包名。

包的命名

  • 给包命名的惯例是使用包所在目录的名字
  • 给包及其目录命名时,应该使用简洁、清晰且全小写的名字

记住:并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。

main包

在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。

包的导入

导入包需要使用关键字import ,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中。

import (
	"fmt"
	"strings"
)

编译器查找packge的顺序:

  1. 标准库,如 /usr/local/go/src/*/*,这个路径由环境变量GOROOT决定。
  2. 当前项目源代码路径,如 /home/myproject/src/*/*
  3. 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 - 常量

Golang 常量

常量的定义

常量的定义与变量类似,只不过使用 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 - 变量

Golang 变量

变量定义

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!"
}

函数外的每个语句都必须以关键字开始(varfunc、等等),:= 结构不能使用在函数外。

多赋值模式

上面同时赋值多个变量的模式,可以用于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 - 指针

Golang 指针

Go 具有指针。 指针保存了变量的内存地址。

基本语法

定义:类型 *T 是指向类型 T 的值的指针。其零值是 nil

var p *int

生成:& 符号会生成一个指向其作用对象的指针。

i := 42
p = &i

取值:* 符号表示指针指向的底层的值。

fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

注意:与 C 不同,Go 没有指针运算

5 - 函数

Golang 函数

语法

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

Golang 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

Golang 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]

如同数组一样,分片总是一维的但可以通过组合来构造高维的对象。数组间组合时,被构造的内部数组总是拥有相同的长度;但分片与分片(或数组与分片)组合时,内部的长度可能是动态变化的。此外,内部分片必须单独初始化。