1 - 概述

Golang 类型概述

Type

https://golang.org/ref/spec#Types

类型决定了一组值,以及对这些值的特定操作和方法。如果有类型名称,可以用类型名称来表示,也可以用类型字面量来指定,由现有的类型组成一个类型。

Type      = TypeName | TypeLit | "(" Type ")" .
TypeName  = identifier | QualifiedIdent .
TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
	    SliceType | MapType | ChannelType .

语言预先声明某些类型名称。其他类型是通过类型声明引入的。复合类型–数组、结构体、指针、函数、接口、切片、映射和通道类型–可以使用类型字面量来构造。

每个类型T都有一个底层类型(underlying type)。如果T是预定义的布尔类型、数值类型、字符串类型之一,或者是类型字面量,那么对应的底层类型就是T本身。否则,T的底层类型是T在其类型声明中引用的类型的底层类型。

type (
	A1 = string
	A2 = A1
)

type (
	B1 string
	B2 B1
	B3 []B1
	B4 B3
)

string、A1、A2、B1、B2的底层类型是string。[]B1、B3和B4的底层类型是[]B1。

方法集

类型可以有一个与之相关的方法集。接口类型的方法集就是它的接口。任何其他类型T的方法集由所有用接收者类型T声明的方法组成。相应的指针类型*T的方法集是用接收者 *TT声明的所有方法的集合(也就是说,它也包含T的方法集)。更多的规则适用于包含嵌入式字段的结构体,如结构体类型一节所述。任何其他类型的方法集都是空的。在方法集中,每个方法必须有一个唯一的非空方法名。

类型的方法集决定了该类型实现的接口和可以使用该类型的接收者调用的方法。

类型和值的属性

https://golang.org/ref/spec#Properties_of_types_and_values

类型一致性

两个类型要么相同,要么不同。

已定义类型(defined type)总是不同于其他类型。否则,如果它们的底层类型字面量在结构上是等价的,那么两个类型就是相同的;也就是说,它们具有相同的字面量结构,相应的组件具有相同的类型。详言之。

  • 如果两个数组类型具有相同的元素类型和相同的数组长度,那么它们就是相同的。
  • 如果两个分片类型具有相同的元素类型,则它们是相同的。
  • 如果两个结构体类型具有相同的字段序列,并且对应的字段具有相同的名称、相同的类型和相同的标签,则两个结构体类型是相同的。来自不同包的非导出字段名总是不同的。
  • 如果两个指针类型具有相同的基础类型,那么它们就是相同的。
  • 如果两个函数类型具有相同数量的参数和结果值,对应的参数和结果类型相同,并且两个函数都是变量,或者都不是变量,那么这两个函数类型就是相同的。参数和结果名称不需要匹配
  • 如果两个接口类型具有相同的方法集,名称相同,函数类型相同,则两个接口类型是相同的。来自不同包的非导出方法名总是不同的。方法的顺序无关紧要
  • 如果两个映射类型有相同的键和元素类型,那么它们就是相同的。
  • 如果两个通道类型具有相同的元素类型和相同的方向,那么它们就是相同的。

给定声明:

type (
	A0 = []string
	A1 = A0
	A2 = struct{ a, b int }
	A3 = int
	A4 = func(A3, float64) *A0
	A5 = func(x int, _ float64) *[]string
)

type (
	B0 A0
	B1 []string
	B2 struct{ a, b int }
	B3 struct{ a, c int }
	B4 func(int, float64) *B0
	B5 func(x int, y float64) *A1
)

type	C0 = B0

这些类型是相同的:

A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5

B0 and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5

B0和B1是不同的,因为它们是由不同的类型定义创建的新类型;func(int,float64) *B0和func(x int,y float64) *[]string是不同的,因为B0与[]string不同。

可分配性

如果以下条件之一适用,则值x可分配给类型T的变量("x is assignable to T")。

  • x的类型与T相同。
  • x的类型V和T的底层类型相同,并且V或T中至少有一个不是定义类型。
  • T是一个接口类型,x实现了T。
  • x是双向通道值,T是通道类型,x的类型V和T具有相同的元素类型,V或T中至少有一个不是定义类型。
  • x是预先声明的标识符nil,T是指针、函数、片、映射、通道或接口类型。
  • x是一个可由T类型的值表示的非类型常量。

可表示性

如果下列条件之一适用,则常数x可由类型为T的值表示:

  • x在由T决定的值集合中。
  • T是一个浮点类型,并且x可以被四舍五入到T的精度而不会溢出。四舍五入使用IEEE 754四舍五入到偶数的规则,但将IEEE负零进一步简化为无符号零。注意,常量值永远不会产生IEEE负零、NaN或无穷大。
  • T是复数类型,x的分量real(x)和 imag(x)可以用T的分量类型(float32或float64)的值表示。
x                   T           x is representable by a value of T because

'a'                 byte        97 is in the set of byte values
97                  rune        rune is an alias for int32, and 97 is in the set of 32-bit integers
"foo"               string      "foo" is in the set of string values
1024                int16       1024 is in the set of 16-bit integers
42.0                byte        42 is in the set of unsigned 8-bit integers
1e10                uint64      10000000000 is in the set of unsigned 64-bit integers
2.718281828459045   float32     2.718281828459045 rounds to 2.7182817 which is in the set of float32 values
-1e-1000            float64     -1e-1000 rounds to IEEE -0.0 which is further simplified to 0.0
0i                  int         0 is an integer value
(42 + 0i)           float32     42.0 (with zero imaginary part) is in the set of float32 values
x                   T           x is not representable by a value of T because

0                   bool        0 is not in the set of boolean values
'a'                 string      'a' is a rune, it is not in the set of string values
1024                byte        1024 is not in the set of unsigned 8-bit integers
-1                  uint16      -1 is not in the set of unsigned 16-bit integers
1.1                 int         1.1 is not an integer value
42i                 float32     (0 + 42i) is not in the set of float32 values
1e1000              float64     1e1000 overflows to IEEE +Inf after rounding

2 - 布尔类型

Golang 布尔类型

Boolean types

https://golang.org/ref/spec#Boolean_types

布尔类型表示由预先声明的常量 true 和 false 表示的布尔真值的集合。预先声明的布尔类型是bool;它是一个已定义类型。

3 - 数字类型

Golang 数字类型

Numeric types

https://golang.org/ref/spec#Numeric_types

数值类型表示整数或浮点值的集合。预先声明的与架构无关的数值类型有:

uint8       the set of all unsigned  8-bit integers (0 to 255)
uint16      the set of all unsigned 16-bit integers (0 to 65535)
uint32      the set of all unsigned 32-bit integers (0 to 4294967295)
uint64      the set of all unsigned 64-bit integers (0 to 18446744073709551615)

int8        the set of all signed  8-bit integers (-128 to 127)
int16       the set of all signed 16-bit integers (-32768 to 32767)
int32       the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64       the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

float32     the set of all IEEE-754 32-bit floating-point numbers
float64     the set of all IEEE-754 64-bit floating-point numbers

complex64   the set of all complex numbers with float32 real and imaginary parts
complex128  the set of all complex numbers with float64 real and imaginary parts

byte        alias for uint8
rune        alias for int32

一个n位整数的值是n位宽,用二补码算术(two’s complement arithmetic)表示。

还有一组预先声明的数值类型,其大小与实现有关。

uint     either 32 or 64 bits
int      same size as uint
uintptr  an unsigned integer large enough to store the uninterpreted bits of a pointer value

为了避免可移植性问题,所有的数值类型都是已定义类型,因此除了byte(uint8的别名)和rune(int32的别名)之外,其他类型都是不同的。当在表达式或赋值中混合使用不同的数值类型时,需要进行显式转换。例如,int32和int不是同一类型,即使它们在特定架构上可能具有相同的大小。

4 - string类型

Golang string类型

String types

https://golang.org/ref/spec#String_types

字符串类型表示字符串值的集合。一个字符串值是一个(可能是空的)字节序列。字节数称为字符串的length/长度,永远不会是负数。字符串是不可改变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是字符串,它是已定义类型。

字符串s的长度可以通过内置函数len来发现。如果字符串是常量,那么长度就是编译时常量。字符串的字节可以通过整数索引0到len(s)-1来访问。取这种元素的地址是非法的;如果 s[i] 是字符串的第 i’th 个字节,&s[i] 是无效的。

参考资料

5 - 数组类型

Golang 数组类型

go语言实战

类型 [n]T 是一个数组,有 n 个类型为 T 的值。

数组定义访问如下,需要指定类型和数组大小:

var a [10]int

注意:数组的长度是其类型的一部分,因此数组不能改变大小

也可以在定义时直接创建数组,数组大小的设置可以有多种方式:

a := [2]string{"a", "b"}
a := []string{"a", "b"}
a := [...]string{"a", "b"}

通过下标访问单个元素:

var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

golang语言规范

https://golang.org/ref/spec#Array_types

数组是一个单一类型的元素的计数序列,称为元素类型。元素的数量称为数组的长度,绝不是负数。

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

长度是数组类型的一部分;它必须计算为一个非负常数,用int类型的值表示。数组a的长度可以通过内置函数len来计算。元素可以用0到len(a)-1的整数索引来寻址。数组类型总是一维的,但可以组成多维类型。

[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64  // same as [2]([2]([2]float64))

Effective Go

https://golang.org/doc/effective_go.html#arrays

在规划内存的详细布局时,数组很有用,有时可以帮助避免分配,但主要是它们是切片的构建模块,也就是下一节的主题。为了给这个主题打下基础,下面说说关于数组的一些情况。

在Go和C中,数组的工作方式有很大的不同,在Go中:

  • 数组就是值。将一个数组赋值给另一个数组会复制所有的元素。
  • 特别是,如果你把一个数组传递给一个函数,它将收到一个数组的副本,而不是一个指向它的指针。
  • 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。

值属性可能很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指针给数组。

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

但即使这种风格也不是 go 的习惯用法。请用分片(slice)代替。

实现原理

参考文章: go语言数组的实现原理

初始化

Go 语言中的数组有两种不同的创建方式,一种是显式的指定数组的大小,另一种是使用 [...]T 声明数组,Go 语言会在编译期间通过源代码对数组的大小进行推断:

arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}

编译器会对第二种数组的大小进行推导,通过遍历元素的方式来计算数组中元素的数量。

这两种初始化方式在运行时是完全等价的,[...]T 这种初始化方式只是 Go 语言提供的一种语法糖,当不想计算数组中的元素个数时可以减少一些工作。

对于一个由字面量组成的数组,根据数组元素数量的不同,编译器会做两种不同的优化:

  1. 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
  2. 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;

访问和赋值

无论是在栈上还是静态存储区,数组在内存中其实就是一连串的内存空间。

表示数组的方法就是:

  1. 一个指向数组开头的指针
  2. 数组中元素的数量
  3. 数组中元素类型占的空间大小

数组访问越界是非常严重的错误,Go 语言中对越界的判断:

  1. 可以在编译期间由静态类型检查完成的,函数会对访问数组的索引进行验证。数组和字符串的一些简单越界错误都会在编译期间发现,比如我们直接使用整数或者常量访问数组。
  2. 使用变量去访问数组或者字符串时,需要Go 语言运行时在发现数组、切片和字符串的越界操作触发程序的运行时错误

访问数组:

  • 在使用字面量整数访问数组下标时就会生成非常简单的中间代码
  • 当编译器无法对数组下标是否越界无法做出判断时才会加入 PanicBounds 指令交给运行时进行判断

参考资料

6 - Slice类型

Golang Slice类型

Slice

摘录自 go语言实战

slice 指向一序列的值,并且包含了长度信息。

[]T 是一个元素类型为 T 的 slice。

p := []int{2, 3, 5, 7, 11, 13}

对 slice 切片

slice 可以重新切片,创建一个新的 slice 值指向相同的数组。

表达式s[lo:hi]表示从 lohi-1 的 slice 元素,含两端。因此s[lo:lo]是空的,而s[lo:lo+1]有一个元素。

p := []int{2, 3, 5, 7, 11, 13}
fmt.Println("p ==", p)
fmt.Println("p[1:4] ==", p[1:4])

// 省略下标代表从 0 开始
fmt.Println("p[:3] ==", p[:3])

// 省略上标代表到 len(s) 结束
fmt.Println("p[4:] ==", p[4:])

构造 slice

slice 由函数 make 创建,第二个参数为数组长度:

a := make([]int, 5)  // len(a)=5,cap(a)=5

可以通过第三个参数来指定容量:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

注意:slice的长度可以在构造时通过参数明确指定,也可以在切片时通过上下两个下标计算而来。而容量则需要考虑左下标开始位置。

func main() {
	a := make([]int, 5)	// 指定长度为5,容量没有设置,则和长度相同:len=5 cap=5
	printSlice("a", a)
	b := make([]int, 0, 5) // 指定长度为0,容量为5:len=0 cap=5
	printSlice("b", b)
	c := b[:2] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从0开始:len=2 cap=5
	printSlice("c", c)
	d := c[2:5] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从2开始:len=3 cap=5
	printSlice("d", d)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

slice 的零值是 nil。一个 nil 的 slice 的长度和容量是 0。

var z []int
fmt.Println(z, len(z), cap(z))

向 slice 添加元素

Go 提供了一个内建函数 append 向 slice 添加元素:

func append(s []T, vs ...T) []T
  • append 的第一个参数 s 是一个类型为 T 的数组,其余类型为 T 的值将会添加到 slice。
  • append 的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。
  • 如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。
func main() {
	var a []int
	printSlice("a", a)

	// append works on nil slices.
	a = append(a, 0)
	printSlice("a", a)

	// the slice grows as needed.
	a = append(a, 1)
	printSlice("a", a)

	// we can add more than one element at a time.
	a = append(a, 2, 3, 4)
	printSlice("a", a)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

Slice types

https://golang.org/ref/spec#Slice_types

分片是底层数组的连续段的描述符,它提供了对该数组中元素的编号序列的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量称为分片的长度,并且永远不会是负数。未初始化的分片的值为零。

SliceType = "[" "]" ElementType .

片段s的长度可以通过内置函数len发现;与数组不同,它可能在执行过程中发生变化。元素可以通过0到len(s)-1的整数索引来寻址。一个给定元素的分片索引可能小于底层数组中相同元素的索引。

分片一旦被初始化,总是与持有其元素的底层数组相关联。因此,一个分片与它的数组和同一数组的其他分片共享存储;相反,不同的数组总是代表不同的存储。

分片的底层数组可以延伸到分片的末端。capacity/容量是对该范围的衡量:它是切片的长度和切片之外的数组的长度之和;一个长度不大于该容量的切片可以通过从原始切片中切出一个新的切片来创建。切片的capacity/容量可以通过内置函数cap(a)来发现。

一个新的、初始化的给定元素类型T的分片值是使用内置函数make制作的,它需要一个分片类型和指定长度和可选容量的参数。用make创建的分片总是分配一个新的、隐藏的数组,返回的分片值指向这个数组。也就是说,执行

make([]T, length, capacity)

产生的分片与分配一个数组并对其进行分片一样,所以这两个表达式是等价的:

make([]int, 50, 100)
new([100]int)[0:50]

像数组一样,切片总是一维的,但可以组成更高维的对象。对于数组的数组,根据结构,内部数组总是相同的长度;但是对于切片的切片(或切片的数组),内部长度可能会动态变化。此外,内部切片必须单独初始化。

Slice

https://golang.org/doc/effective_go.html#slices

Slices 包裹了数组,为数据序列提供了一个更通用、更强大、更方便的接口。除了具有显式维度的项目(如变换矩阵),Go 中的大多数数组编程都是通过切片而不是简单的数组来完成的。

切片持有对底层数组的引用,如果您将一个切片分配给另一个切片,则两者都指向同一个数组。如果函数接受切片参数,那么它对切片元素的改变将对调用者可见,类似于传递一个指向底层数组的指针。因此,Read函数可以接受切片参数,而不是一个指针和一个计数;切片中的长度设置了一个读取数据的上限。这里是包os中File类型的Read方法的签名。

func (f *File) Read(buf []byte) (n int, err error)

该方法返回读取的字节数和一个错误值(如果有的话)。要读入一个较大的缓冲区buf的前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 slice时是合法的,并返回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
}

我们必须在事后返回slice,因为虽然Append可以修改slice的元素,但slice本身(持有指针、长度和容量的运行时数据结构)是通过值传递的。

对 slice 进行追加的想法非常有用,它被 append 内置函数所捕获。不过要理解那个函数的设计,我们需要更多的信息,所以我们稍后会回到它。

Two-dimensional slices

https://golang.org/doc/effective_go.html#two_dimensional_slices

TODO:后面再细看

参考资料

7 - 结构体类型

Golang 结构体类型

Struct

摘录自 go语言实战

结构体是字段的集合。结构体定义的语法:

type Vertex struct {
	X int
	Y int
}

访问范围通过结构体和字段名的首字母大小写来体现:大写为public,小写为private。

构建结构体

可以通过值列表给结构体的各个字段赋值,新分配一个结构体,顺序和结构体定义的字段顺序一致。也可以通过使用 Name: 语法为单个字段赋值,未明确赋值的字段则取缺省值。

var (
	v1 = Vertex{1, 2}  // 类型为 Vertex
	v2 = Vertex{X: 1}  // Y:0 被省略
	v3 = Vertex{}      // X:0 和 Y:0
	p  = &Vertex{1, 2} // 类型为 *Vertex
)

特殊的前缀 & 返回一个指向结构体的指针。

访问字段

通过点号访问结构体的字段:

v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)

也可以通过指针访问:

v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)

Struct types

https://golang.org/ref/spec#Struct_types

结构体是命名元素(称为字段)的序列,每个字段都有name/名称和type/类型。字段名可以显式(IdentifierList)或隐式(EmbeddedField)指定。在结构体中,非空白的字段名必须是唯一的。

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag           = string_lit .
// An empty struct.
struct {}

// A struct with 6 fields.
struct {
	x, y int
	u float32
	_ float32  // padding
	A *[]int
	F func()
}

一个声明了类型但没有显式字段名的字段称为嵌入式字段。嵌入式字段必须指定为类型名T或指向非接口类型名*T的指针,T本身不能是指针类型。非限定的类型名作为字段名。

// A struct with four embedded fields of types T1, *T2, P.T3 and *P.T4
struct {
	T1        // field name is T1
	*T2       // field name is T2
	P.T3      // field name is T3
	*P.T4     // field name is T4
	x, y int  // field names are x and y
}

以下声明是非法的,因为字段名在结构类型中必须是唯一的。

struct {
	T     // conflicts with embedded field *T and *P.T
	*T    // conflicts with embedded field T and *P.T
	*P.T  // conflicts with embedded field T and *T
}

如果x.f是表示该字段或方法f的合法选择器,那么结构体x中嵌入字段的字段或方法f被称为promoted(提升)。

被提升的字段与结构体的普通字段一样,只是它们不能在结构体的复合字面量中作为字段名使用。

给定一个结构类型S和一个已定义类型T,被提升的方法被包含在结构体的方法集中,具体如下:

  • 如果S包含一个嵌入字段T,那么S和*S的方法集都包含有接收者T的提升方法。*S的方法集还包括具有接收者*T的提升方法。
  • 如果S包含一个内嵌字段*T,那么S和*S的方法集都包含有接收者T或*T的被提升方法。

字段声明后面可以有一个可选的字符串字面量标签(tag),它成为对应字段声明中所有字段的属性。空的标签字符串相当于一个不存在的标签。标签通过反射接口变得可见,并参与结构体的类型识别,但在其他方面被忽略。

struct {
	x, y float64 ""  // an empty tag string is like an absent tag
	name string  "any string is permitted as a tag"
	_    [4]byte "ceci n'est pas un champ de structure"
}

// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

参考资料

8 - 指针类型

Golang 指针类型

Pointer types

https://golang.org/ref/spec#Pointer_types

PointerType = "*" BaseType .
BaseType    = Type .
*Point
*[4]int

参考资料

9 - 函数类型

Golang 函数类型

Function types

https://golang.org/ref/spec#Function_types

函数类型表示具有相同参数和结果类型的所有函数的集合。未初始化的函数类型变量的值为零。

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

在参数或结果的列表中,名称(IdentifierList)必须全部存在或全部不存在。如果存在,每个名称代表指定类型的一个项目(参数或结果),并且签名中所有非空白名称必须是唯一的。如果不存在,每个类型代表该类型的一个项目。参数和结果列表总是用括号表示,但如果正好有一个未命名的结果,则可以写成一个未括号的类型。

在函数签名中,最后一个输入的参数可以有一个以…为前缀的类型。带有这样参数的函数被称为 variadic 可变参数,可以用零或多个参数来调用该参数。

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

10 - 接口类型

Golang 接口类型

Interface types

https://golang.org/ref/spec#Interface_types

接口类型指定了一个称为 interface/接口的方法集。接口类型的变量可以存储任何类型的值,其方法集是接口的任何超集。这样的类型被称为实现了接口。未初始化的接口类型变量的值是nil。

InterfaceType      = "interface" "{" { ( MethodSpec | InterfaceTypeName ) ";" } "}" .
MethodSpec         = MethodName Signature .
MethodName         = identifier .
InterfaceTypeName  = TypeName .

接口类型可以通过方法规范明确地指定方法,也可以通过接口类型名称嵌入其他接口的方法。

// A simple File interface.
interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Close() error
}

每个显式指定方法的名称必须是唯一的,不能是空白的。

interface {
	String() string
	String() string  // illegal: String not unique
	_(x int)         // illegal: method must have non-blank name
}

可以有多个类型实现一个接口。例如,如果两个类型S1和S2的方法集为

func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error

(其中T代表S1或S2),那么File接口是由S1和S2同时实现的,不管S1和S2可能有什么其他方法或共享什么方法。

类型实现了由其方法的任何子集组成的任何接口,因此可以实现几个不同的接口。例如,所有类型都实现空接口。

interface{}

类似地,考虑这个接口规范,它出现在一个类型声明中,定义了一个叫做Locker的接口。

type Locker interface {
	Lock()
	Unlock()
}

如果S1和S2也实现:

func (p T) Lock() {  }
func (p T) Unlock() {  }

它们实现了Locker接口和File接口。

接口T可以使用一个(可能是限定的)接口类型名E来代替方法规范。这就是所谓的在T中嵌入(embedding)接口E。 T的方法集是T的显式声明方法和T的嵌入式接口的方法集的联合。

type Reader interface {
	Read(p []byte) (n int, err error)
	Close() error
}

type Writer interface {
	Write(p []byte) (n int, err error)
	Close() error
}

// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
	Reader  // includes methods of Reader in ReadWriter's method set
	Writer  // includes methods of Writer in ReadWriter's method set
}

方法集的联合体包含了每个方法集的(导出的和未导出的)方法,每个方法集只有一次,而且名称相同的方法必须有相同的签名。

type ReadCloser interface {
	Reader   // includes methods of Reader in ReadCloser's method set
	Close()  // illegal: signatures of Reader.Close and Close are different
}

接口类型T不得将自己或任何嵌入T的接口类型,递归地嵌入。

// illegal: Bad cannot embed itself
type Bad interface {
	Bad
}

// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

Interface

https://golang.org/doc/effective_go.html#interfaces

Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。

类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 Len()、Less(i, j int) bool 和 Swap(i, j int),它还可以有一个自定义的格式器。在这个人为的例子中,Sequence同时满足这两个条件。

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

11 - Map类型

Golang Map类型

Map

备注:内容摘录自 go语言实战

map 在使用之前必须用 make 而不是 new 来创建;值为 nil 的 map 是空的,并且不能赋值。

m := make(map[string]string) // 语法是 "map[key的类型]value的类型"
m["key1"] = "value1"
fmt.Println(m["key1"])

或者直接通过指定key、value来创建:

var m = map[string]string{
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",	// 注意最后一行的结尾也必须有逗号
}

value的类型可以忽略,比如下面这种写法:

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

可以简化为:

var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

修改 map:

func main() {
	m := make(map[string]int)

	m["Answer"] = 42	//在 map 中插入或修改
	fmt.Println("The value:", m["Answer"])  // 通过key获取值

	m["Answer"] = 48 //在 map 中插入或修改
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer") // 从 map 中删除key
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"] // 双赋值检测某个键存在,如果存在则第二个参数返回true
	fmt.Println("The value:", v, "Present?", ok)
}

Map types

https://golang.org/ref/spec#Map_types

Map是由一种类型(称为元素类型)的元素组成的无序组,由另一种类型的一组唯一键(称为键类型)索引。未初始化的map的值为nil。

MapType     = "map" "[" KeyType "]" ElementType .
KeyType     = Type .

比较运算符 == 和 != 必须为键类型的操作数完全定义;因此键类型不能是函数、映射或分片。如果键类型是接口类型,必须为动态键值定义这些比较运算符,否则会引起运行时的恐慌。

map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

Map元素的数量称为其长度。对于map m来说,它可以通过内置函数len来发现,并且在执行过程中可能会改变。在执行过程中可以使用赋值来添加元素,并使用索引表达式来检索;可以使用内置的 delete 函数来删除元素。

使用内置函数make制作一个新的、空的map值,该函数将map类型和一个可选的容量提示作为参数。

make(map[string]int)
make(map[string]int, 100)

初始容量并不约束其大小:为了容纳其中存储的项目数量map可以增长,但 nil map除外。nil map相当于一个空map,但不能添加任何元素。

Maps

https://golang.org/doc/effective_go.html#maps

Map是一种方便而强大的内置数据结构,它将一种类型的值(键)与另一种类型的值(元素或值)关联起来。键可以是任何定义了 equality 运算符的类型,如整数、浮点和复数、字符串、指针、接口(只要动态类型支持equality)、结构体和数组。切片不能用作映射键,因为在它们上面没有定义equality。和切片一样,map 也持有对底层数据结构的引用。如果你把一个map传给一个函数,该函数改变了map的内容,那么这些改变将在调用者中可见。

Map可以使用通常的复合文字语法和冒号分隔的键值对来构建,所以在初始化时很容易构建它们。

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

分配和获取映射值在语法上看起来就像对数组和切片做同样的事情一样,只是索引不需要是一个整数。

offset := timeZone["EST"]

试图用map中不存在的键来获取一个map值,将返回map中条目类型的0值。例如,如果map中包含整数,查找一个不存在的键将返回0。集合可以实现为一个值类型为bool的map。将map条目设置为true,将值放入集合中,然后通过简单的索引进行测试。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

有时你需要区分缺失的条目和零值。是否有 “UTC “的条目,还是因为map中根本没有这个条目而为0?你可以用一种多重赋值的形式来区分。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

出于明显的原因,这被称为 “逗号 ok “习惯用法。在这个例子中,如果tz存在,秒数将被适当设置,ok将为true;如果不存在,秒数将被设置为0,ok将为false。这里有一个函数,把它和一个漂亮的错误报告放在一起。

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

为了测试map中是否存在,而不用担心实际值,你可以使用空白标识符(_)来代替通常的变量的值。

_, present := timeZone[tz]

要删除一个map条目,使用delete内置函数,其参数是map和要删除的键。即使key已经不在map上,这样做也是安全的。

delete(timeZone, "PDT")  // Now on Standard Time

参考资料

12 - Channel类型

Golang Channel类型

Channel types

https://golang.org/ref/spec#Channel_types

通道为并发执行的函数提供了一种机制,通过发送和接收指定元素类型的值进行通信。未初始化的通道的值为nil。

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

可选的 <- 操作符指定通道方向,发送或接收。如果没有给出方向,则通道是双向的。通道可以通过赋值或显式转换来限制只能发送或只能接收。

chan T          // can be used to send and receive values of type T
chan<- float64  // can only be used to send float64s
<-chan int      // can only be used to receive ints

<-操作符与最左边的chan可能关联。

chan<- chan int    // same as chan<- (chan int)
chan<- <-chan int  // same as chan<- (<-chan int)
<-chan <-chan int  // same as <-chan (<-chan int)
chan (<-chan int)

可以使用内置的函数make制作一个新的、初始化的通道值,该函数将通道类型和一个可选的容量作为参数。

make(chan int, 100)

capacity,以元素数为单位,设置通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有当发送方和接收方都准备好时,通信才会成功。否则,通道被缓冲,如果缓冲区没有满(发送)或不空(接收),则通信成功而不阻塞。nil通道永远不会为通信做好准备。

可以用内置函数close关闭通道。接收运算符的多值赋值形式报告在通道关闭前是否有接收值被发送。

单个通道可以用于发送语句、接收操作,以及任何数量的goroutine对内置函数cap和len的调用,而无需进一步同步。通道是先入先出(first-in-first-out)的队列。例如,如果goroutine在通道上发送数值,而第二个goroutine接收数值,那么数值将按照发送的顺序接收。