Loading... ## 基础 ### 包 package 每个Go程序都是由**包(package)** 构成 程序从`main包`开始运行 按照约定,包名与导入路径的最后一个元素一致。例如,`"math/rand"` 包中的源码均以 `package rand` 语句开始。 ```go package main import ( "fmt" "math/rand" ) func main() { fmt.Println("My favorite number is", rand.Intn(10)) } ``` ### 导入 import 单个调用语句: ```go import "包名" ``` 分组导入调用: ```go import ( "包名1" "包名2" ) ``` ### 导出名 在 Go 中,如果一个名字**以大写字母开**头,那么它就是**已导出的**。例如,`Pizza` 就是个已导出名,`Pi` 也同样,它导出自 `math` 包。 `pizza` 和 `pi` 以小写开头,所以它们是未导出的。 在**导入一个包时,你只能引用其中已导出的名字**。任何“未导出”的名字在该包外均无法访问。 使用`Math.pi`错误输出。 ``` ./prog.go:9:14: cannot refer to unexported name math.pi ``` 然后改名为 `math.Pi` 执行成功。 ### 函数 func 函数声明中 - **类型在变量名之后** - **返回类型在参数列表后** - **函数体的左括号必须在第一行** ```go func add(x int, y int) int { return x + y } ``` 当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。 ```go x int, y int x, y int ``` ### 返回值 return #### 多值返回 函数可以返回多个值,**多个返回类型用括号列出** ```go package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a, b) } ``` #### 命名返回值 Go 的返回值可被命名,在返回类型列表中使用**与参数列表同样的命名方式**,它们会被视作**定义在函数顶部的变量**。 ```go package main import "fmt" func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return } func main() { fmt.Println(split(17)) } ``` 返回值的名称应当具有一定的意义,它可以作为文档使用。 没有参数的 `return` 语句返回已命名的返回值。也就是 `直接` 返回。 直接返回语句应当仅用在短函数中。在长的函数中它们会影响代码的可读性。 ### 变量 var `var` 语句用于**声明一个变量列表**,跟函数的参数列表一样,类型在最后。 `var` 语句可以出现在包或函数级别。 ```go package main import "fmt" var c, python, java bool func main() { var i int fmt.Println(i, c, python, java) } ``` #### 变量初始化 变量声明可以包含初始值,每个变量对应一个。 **如果初始化值已存在,则可以省略类型**;变量会从初始值中自动获得类型。 ```go package main import "fmt" var i, j int = 1, 2 func main() { var c, python, java = true, false, "no!" fmt.Println(i, j, c, python, java) } ``` #### 短变量声明 在函数中,简洁赋值语句 `:=` 可在**类型明确**的地方**代替 `var` 声明** 函数外的每个语句都必须以关键字开始(`var`, `func` 等等),因此 **`:=` 结构不能在函数外使用** ```go var i, j int = 1, 2 k := 3 c, python, java := true, false, "no!" ``` ### 类型 #### 基本类型 Go 的基本类型有 ``` bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 的别名 rune // int32 的别名 // 表示一个 Unicode 码点 float32 float64 complex64 complex128 ``` 本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。 `int`, `uint` 和 `uintptr` 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 `int` 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。 #### 零值 **没有明确初始值的变量声明会被赋予它们的零值。** 零值是: - 数值类型为 `0`, - 布尔类型为 `false`, - 字符串为 `""`(空字符串)。 #### 类型转换 表达式 `T(v)` 将值 `v` 转换为类型 `T`。 一些关于数值的转换: ``` var i int = 42 var f float64 = float64(i) var u uint = uint(f) ``` 或者,更加简单的形式: ``` i := 42 f := float64(i) u := uint(f) ``` 与 C 不同的是,**Go 在不同类型的项之间赋值时需要显式转换**。 移除例子中 `float64` 或 `uint` 的转换,程序报错: ``` ./prog.go:11:6: cannot use f (type float64) as type uint in assignment ``` #### 类型推导 在声明一个变量而**不指定其类型时**(即使用不带类型的 `:=` 语法或 `var =` 表达式语法),**变量的类型由右值推导得出。** 当右值声明了类型时,新变量的类型与其相同: ``` var i int j := i // j 也是一个 int ``` 不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 `int`, `float64` 或 `complex128` 了,这取决于常量的精度: ``` i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128 ``` #### 常量 常量的声明与变量类似,只不过是使用 `const` 关键字。 常量可以是字符、字符串、布尔值或数值。 常量不能用 `:=` 语法声明。 ```go package main import "fmt" const Pi float32 = 3.14 func main() { const World = "世界" fmt.Println("Hello", World) fmt.Println("Happy", Pi, "Day") const Truth bool = true fmt.Println("Go rules?", Truth) } ``` ``` Hello 世界 Happy 3.14 Day Go rules? true ``` ## 流程控制语句 ### for 循环语句 **Go 只有一种循环结构:`for` 循环。** ```go package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) } ``` 基本的 `for` 循环由三部分组成,它们用分号隔开: - 初始化语句:在第一次迭代前执行 - 条件表达式:在每次迭代前求值 - 后置语句:在每次迭代的结尾执行 **注意**:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 `{ }` 则是必须的。 - 但是我们可以去掉for语句中的分号,以**达到while的效果** ```go package main import "fmt" func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) } ``` - 如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。 ```go package main func main() { for { } } ``` ### if 条件语句 Go 的 `if` 语句与 `for` 循环类似,表达式外无需小括号 `( )` ,而大括号 `{ }` 则是必须的。 ```go func sqrt(x float64) string { if x < 0 { return sqrt(-x) + "i" } return fmt.Sprint(math.Sqrt(x)) } ``` 同 `for` 一样, `if` 语句可以在条件表达式前执行一个简单的语句。 ```go func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } return lim } ``` `if`可接`else`,`if` 语句中声明的变量作用域包含`else` ```go func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } else { fmt.Printf("%g >= %g\n", v, lim) } return lim } ``` ### switch 条件语句 Go 自动提供了在这些语言中每个 case 后面所需的 `break` 语句。 除非以 `fallthrough` 语句结束,否则分支会自动终止。 ```go package main import ( "fmt" "runtime" ) func main() { fmt.Print("Go runs on ") switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: // freebsd, openbsd, // plan9, windows... fmt.Printf("%s.\n", os) } } ``` #### 没有条件的 switch 没有条件的 switch 同 `switch true` 一样。 这种形式能将一长串 if-then-else 写得更加清晰。 ```go package mainimport ( "fmt" "time")func main() { t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon.") default: fmt.Println("Good evening.") }} ``` ### defer defer 语句会将函数推迟到外层函数返回之后执行。 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。 ```go package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") } ``` 输出: ``` hello world ``` 推迟的函数调用会被压入一个**栈**中。当外层函数返回时,被推迟的函数会按照**后进先出**的顺序调用。 更多关于 defer 语句的信息,请阅读[此博文](http://blog.go-zh.org/defer-panic-and-recover)。 ## 进阶类型 ### 指针 Go 拥有指针。指针保存了值的内存地址。 类型 `*T` 是指向 `T` 类型值的指针。其零值为 `nil`。 ``` var p *int ``` `&` 操作符会生成一个指向其操作数的指针。 ``` i := 42 p = &i ``` `*` 操作符表示指针指向的底层值。 ``` fmt.Println(*p) // 通过指针 p 读取 i*p = 21 // 通过指针 p 设置 i ``` 这也就是通常所说的“**间接引用**”或“重定向”。 ***与 C 不同,Go 没有指针运算。*** ### 结构体 ```go package main import "fmt" type Vertex struct { X int Y int } func main() { fmt.Println(Vertex{1, 2}) } ``` #### 结构体文法(构造) 结构体文法通过直接列出字段的值来新分配一个结构体。 使用 `Name:` 语法可以仅列出部分字段。(字段名的顺序无关) 特殊的前缀 `&` 返回一个指向结构体的指针。 ```go package main import "fmt" type Vertex struct { X, Y int } var ( v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体 v2 = Vertex{X: 1} // Y:0 被隐式地赋予 v3 = Vertex{} // X:0 Y:0 p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针) ) func main() { fmt.Println(v1, v2, v3, p) } ``` 输出: ``` {1 2} {1 0} {0 0} &{1 2} ``` #### 结构体访问 结构体字段使用**点号**来访问。 ```go v := Vertex{1, 2} v.X = 4 fmt.Println(v.X) ``` 结构体字段可以通过**结构体指针**来访问。 如果我们有一个指向结构体的指针 `p`,那么可以通过 `(*p).X` 来访问其字段 `X`。 不过这么写太啰嗦了,所以**Go语言也允许我们使用隐式间接引用,直接写 `p.X` 就可以。** ### 数组 类型 `[n]T` 表示拥有 `n` 个 `T` 类型的值的数组。 表达式 ``` var a [10]int ``` 会将变量 `a` 声明为拥有 10 个整数的数组。 数组的长度是其类型的一部分,因此**数组不能改变大小**。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。 ### 切片 每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。 类型 `[]T` 表示一个元素类型为 `T` 的切片。 切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔: ``` a[low : high] ``` 它会选择一个半开区间,包括第一个元素,但排除最后一个元素。 以下表达式创建了一个切片,它包含 `a` 中下标从 1 到 3 的元素: ``` a[1:4] ``` 缺省上界或下界会默认下界为 `0`,上界则默认是该切片的长度。 对于数组 ``` var a [10]int ``` 来说,以下切片是等价的: ``` a[0:10] a[:10] a[0:] a[:] ``` 切片并不存储任何数据,它只是描述了底层数组中的一段,**就像数组的引用**。 - 更改切片的元素会修改其底层数组中对应的元素。 - 与它共享底层数组的切片都会观测到这些修改。 #### 切片文法(构造) 切片文法类似于没有长度的数组文法。 这是一个数组文法: ``` [3]bool{true, true, false} ``` 下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片: ``` []bool{true, true, false} ``` **这个可以用作一种静态数组的初始化构造方法** ```go package main import "fmt" func main() { q := []int{2, 3, 5, 7, 11, 13} fmt.Println(q) // [2 3 5 7 11 13] r := []bool{true, false, true, true, false, true} fmt.Println(r) // [true false true true false true] s := []struct { i int b bool }{ {2, true}, {3, false}, {5, true}, {7, true}, {11, false}, {13, true}, } fmt.Println(s) // [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}] } ``` #### 切片的长度和容量 切片拥有 **长度** 和 **容量** 的区分。 - 切片的长度就是它所包含的元素个数。 - 切片的容量是**从它的第一个元素开始数**,到其**底层数组元素末尾**的个数。 切片 `s` 的长度和容量可通过表达式 `len(s)` 和 `cap(s)` 来获取。 #### nil 切片 **切片的零值是 `nil`。** nil 切片的长度和容量为 0 且没有底层数组。 #### 用make创建切片 切片可以用内建函数 `make` 来创建,这也是你**创建动态数组**的方式。 ```go func make(type, len, cap) []T ``` `make` 函数会分配一个**元素全为零值的数组**并返回一个引用了它的切片: ```go a := make([]int, 5) // len(a)=5 ``` 要指定它的容量,需向 `make` 传入第三个参数: ```go b := make([]int, 0, 5) // len(b)=0, cap(b)=5 b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4 ``` #### 向切片追加元素 为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 `append` 函数。 *内建函数的[文档](https://go-zh.org/pkg/builtin/#append)* ```go func append(s []T, vs ...T) []T ``` `append` 的第一个参数 `s` 是一个元素类型为 `T` 的切片,其余类型为 `T` 的值将会追加到该切片的末尾。 `append` 的结果是一个包含原切片所有元素加上新添加元素的切片。 当 `s` 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。 (要了解关于切片的更多内容,请阅读文章 [Go 切片:用法和本质](https://blog.go-zh.org/go-slices-usage-and-internals)。) #### range `for` 循环的 `range` 形式可遍历切片或映射。 ```go package main import "fmt" var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} func main() { for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } } ``` 当使用 `for` 循环遍历切片时,每次迭代都会返回两个值。 第一个值为**当前元素的下标** 第二个值为**该下标所对应元素的一份副本** 可以将下标或值赋予 `_` 来忽略它。 ```go for i, _ := range pow for _, value := range pow ``` 若只需要索引,忽略第二个变量即可。 ```go for i := range pow ``` #### 操作二维数组示例 实现 `Pic`。它应当返回一个长度为 `dy` 的切片,其中每个元素是一个长度为 `dx`,元素类型为 `uint8` 的切片。其中每个位置元素值为 `(x+y)/2` ```go package main import "fmt" func Pic(dx, dy int) [][]uint8 { res := make([][]uint8, dy) for y := range res { res[y] = make([]uint8, dx) for x := range res[y] { res[y][x] = uint8( (x+y)/2 ) } } return res } func main() { for _, a := range Pic(8, 8){ fmt.Println(a) } } ``` 输出: ``` [0 0 1 1 2 2 3 3] [0 1 1 2 2 3 3 4] [1 1 2 2 3 3 4 4] [1 2 2 3 3 4 4 5] [2 2 3 3 4 4 5 5] [2 3 3 4 4 5 5 6] [3 3 4 4 5 5 6 6] [3 4 4 5 5 6 6 7] ``` ### 映射 映射将**键**映射到**值**。 ``` map[keyType]valType ``` 映射的零值为 `nil` 。`nil` 映射既没有键,也不能添加键。 `make` 函数会返回给定类型的映射,并将其**初始化**备用。 ```go package main import "fmt" type Vertex struct { Lat, Long float64 } var m map[string]Vertex func main() { m = make(map[string]Vertex) m["Bell Labs"] = Vertex{ 40.68433, -74.39967, } fmt.Println(m["Bell Labs"]) } ``` #### 映射的文法(构造) 映射的文法与结构体相似,不过必须有键名。 ```go package main import "fmt" type Vertex struct { Lat, Long float64 } var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, }, } ``` 若顶级类型只是一个类型名,你可以在文法的元素中省略它。 ```go var m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408}, } ``` #### 修改映射 在映射 `m` 中插入或**修改元素**: ``` m[key] = elem ``` **获取元素**: ``` elem = m[key] ``` **删除元素**: ``` delete(m, key) ``` 通过双赋值检测**某个键是否存在**: ``` elem, ok = m[key] ``` - 若 `key` 在 `m` 中,`ok` 为 `true` ;否则,`ok` 为 `false`。 - 若 `key` 不在映射中,那么 `elem` 是该映射元素类型的零值。同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。 **注** :若 `elem` 或 `ok` 还未声明,你可以使用短变量声明: ``` elem, ok := m[key] ``` #### 使用映射统计词频示例 ```go func WordCount(s string) map[string]int { ans := make(map[string]int) ss := strings.Fields(s) for _, str := range ss { ans[str] += 1 } return ans } ``` ### 函数值 函数也是值。它们可以像其它值一样传递。 函数值可以用作函数的参数或返回值。 ```go package main import ( "fmt" "math" ) func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) } func main() { hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(5, 12)) fmt.Println(compute(hypot)) fmt.Println(compute(math.Pow)) } ``` Go 函数可以是一个闭包。**闭包是一个函数值,它引用了其函数体之外的变量**。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。 例如,函数 `adder` 返回一个闭包。每个闭包都被绑定在其各自的 `sum` 变量上。 ```go package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } } ``` 输出: ``` 0 0 1 -2 3 -6 6 -12 10 -20 15 -30 21 -42 28 -56 36 -72 45 -90 ``` #### 函数闭包实现斐波那契示例 ```go package main import "fmt" // 返回一个“返回int的函数” func fibonacci() func() int { sum := []int{0, 1} p := 0 return func() int { p += 1 if p <= 2 { return sum[p-1] } sum[0], sum[1] = sum[1], sum[1] + sum[0] return sum[1] } } func main() { f := fibonacci() for i := 0; i < 10; i++ { fmt.Println(f()) } } ``` ## 方法与接口 ### 方法 Go 没有类。不过你可以为**结构体类型**定义**方法**。 方法就是一类带特殊的 **接收者** 参数的函数。 方法接收者在它自己的参数列表内,位于 `func` 关键字和方法名之间。 在此例中,`Abs` 方法拥有一个名为 `v`,类型为 `Vertex` 的接收者。 ```go package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } // 方法 func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) } ``` - 记住:方法只是个带接收者参数的函数。 现在这个 `Abs` 的写法就是个正常的函数,功能并没有什么变化。 ```go // 普通函数 func Abs(v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(Abs(v)) } ``` - 你也可以为非结构体类型声明方法。 - 接收者的类型定义和方法声明必须在同一包内; - **不能为内建类型声明方法**(`int, float64等`)。 在此例中,我们看到了一个带 `Abs` 方法的数值类型 `MyFloat`。 ```go package main import ( "fmt" "math" ) type MyFloat float64 func (f float64) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) } func main() { f := -math.Sqrt(2) fmt.Println(f.Abs()) } ``` #### 指针接收者 可以为指针接收者声明方法。即对于类型T,可以使用*T的文法。 例如,这里为 `*Vertex` 定义了 `Scale` 方法。 **指针接收者的方法可以修改接收者指向的值**(就像 `Scale` 在这做的)。由于<u>方法经常需要修改它的接收者</u>,指针接收者比值接收者更常用。 ```go package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } // 指针接收者 func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func main() { v := Vertex{3, 4} v.Scale(10) fmt.Println(v.Abs()) } ``` 输出 : 50 若是将`func (v *Vertex) `中的*去掉,则结果只会输出: 5 (`Scale`并没有修改v中的XY值,使用的是**值接收者**, `Scale` 方法会对原始 `Vertex` 值的**副本**进行操作) ### 接口 interface **接口类型** 是由**一组方法签名定义的集合**。 接口类型的变量可以**保存任何实现了这些方法的值**。 **注意:** 示例代码的 22 行存在一个错误。由于 `Abs` 方法只为 `*Vertex` (指针类型)定义,因此 `Vertex`(值类型)并未实现 `Abser`。 ```go package main import ( "fmt" "math" ) type Abser interface { Abs() float64 } func main() { var a Abser f := MyFloat(-math.Sqrt2) v := Vertex{3, 4} a = f // a MyFloat 实现了 Abser a = &v // a *Vertex 实现了 Abser // 下面一行,v 是一个 Vertex(而不是 *Vertex) // 所以没有实现 Abser。 a = v fmt.Println(a.Abs()) } type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) } type Vertex struct { X, Y float64 } func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } ``` #### 接口与隐式实现 类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。 隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。 因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。 ```go package main import "fmt" type I interface { M() } type T struct { S string } // 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。 func (t T) M() { fmt.Println(t.S) } func main() { var i I = T{"hello"} i.M() } ``` #### 接口值 接口也是值。它们可以像其它值一样传递。 接口值可以用作函数的参数或返回值。 在内部,接口值可以看做包含值和具体类型的元组: ``` (value, type) ``` 接口值保存了一个具体底层类型的具体值。 接口值调用方法时会执行其底层类型的同名方法。 ```go package main import ( "fmt" "math" ) type I interface { M() } type T struct { S string } // *T实现i的接口 func (t *T) M() { fmt.Println(t.S) } type F float64 // F实现i的接口 func (f F) M() { fmt.Println(f) } func main() { var i I // i是接口值 i = &T{"Hello"} describe(i) i.M() i = F(math.Pi) describe(i) i.M() } func describe(i I) { fmt.Printf("(%v, %T)\n", i, i) } ``` 输出: ``` (&{Hello}, *main.T) Hello (3.141592653589793, main.F) 3.141592653589793 ``` #### 底层值为nil的接口值 即便接口内的具体值为 nil,方法仍然会被 **nil 接收者**调用。 在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 `M` 方法)。 - **注意:** 保存了 nil 具体值的接口其自身并不为 nil。 ```go package main import "fmt" type I interface { M() } type T struct { S string } func (t *T) M() { if t == nil { fmt.Println("<nil>") return } fmt.Println(t.S) } func main() { var i I var t *T // t此时为空 i = t describe(i) i.M() i = &T{"hello"} describe(i) i.M() } func describe(i I) { fmt.Printf("(%v, %T)\n", i, i) } ``` 输出: ``` (<nil>, *main.T) <nil> (&{hello}, *main.T) hello ``` #### 空接口 指定了零个方法的接口值被称为 **空接口**: ``` interface{} ``` - 空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。) - 空接口被用来处理未知类型的值。例如,`fmt.Print` 可接受类型为 `interface{}` 的任意数量的参数。 ```go var i interface{} describe(i) ``` 输出 ``` (<nil>, <nil>) ``` #### 类型断言 **类型断言** 提供了访问接口值底层具体值的方式。 ``` t := i.(T) ``` 该语句断言**接口值 `i` 保存了具体类型 `T`,并将其底层类型为 `T` 的值赋予变量 `t`**。 若 `i` 并未保存 `T` 类型的值,该语句就会触发一个恐慌。 为了 **判断** 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。 ``` t, ok := i.(T) ``` 若 `i` 保存了一个 `T`,那么 `t` 将会是其底层值,而 `ok` 为 `true`。 否则,`ok` 将为 `false` 而 `t` 将为 `T` 类型的零值,程序并不会产生恐慌。 请注意这种语法和读取一个映射时的相同之处。 ## 并发 最后修改:2021 年 08 月 01 日 07 : 45 PM © 允许规范转载