Go语言基础

Go 编程语言是一个开源项目,它使程序员更具生产力。 Go 语言具有很强的表达能力,它简洁、清晰而高效。得益于其并发机制, 用它编写的程序能够非常有效地利用多核与联网的计算机,其新颖的类型系统则使程序结构变得灵活而模块化。 Go 代码编译成机器码不仅非常迅速,还具有方便的垃圾收集机制和强大的运行时反射机制。 它是一个快速的、静态类型的编译型语言,感觉却像动态类型的解释型语言。

语言基础

命名规则

  • 一个名字必须以一个字母(Unicode 字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。
  • 字母区分大小写
  • 首字母大小写标识可访问性,大写字母开头的相当于 class 中的带 public 关键词的公有变量 / 函数;小写字母开头的就是有 private 关键词的私有变量 / 函数。( 对于中文汉字,Unicode 标志都作为小写字母处理,因此中文的命名默认不能导出 )
  • 推荐使用 驼峰式 命名

关键字

break      default       func     interface   select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

Go 语言主要有四种类型的声明语句:varconsttypefunc,分别对应变量、常量、类型和函数实体对象的声明。

内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error

内建函数: make len cap new append copy close delete
complex real imag
panic recover

变量

变量声明

关键字var,而类型信息放在变量名之后 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。

var v2 string
// 声明多个变量
var (
v1 int
v2 string
)

对于声明变量时需要进行初始化的场景,var 关键字可以保留,但不再是必要的元素

var v1 int = 10 // 正确的使用方式1
var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型
v3 := 10 // 正确的使用方式3,编译器可以自动推导出v3的类型

变量赋值

var v10 int
v10 = 123
// 多重赋值
i, j = j, i

常量

const关键字 ,Go 的常量定义可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它 与字面常量一样,是无类型常量。

const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点常量

Go 语言预定义了这些常量:truefalseiota

const (           // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)

const (
a = 1 << iota // a == 1 (iota在每个const开头被重设为0)
b = 1 << iota // b == 2
c = 1 << iota // c == 4
)

如果两个 const 的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上 面的前两个 const 语句可简写为

const (       // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)

const (
a = 1 <<iota // a == 1 (iota在每个const开头被重设为0)
b // b == 2
c // c == 4
)

在 const 后跟一对圆括号的方式定义一组常量,这种定义法在 Go 语言中通常用于定义 枚举值。Go 语言并不支持众多其他语言明确支持的 enum 关键字。

类型

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。

package tempconv

import "fmt"

type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

Celsius 和 Fahrenheit 分别对应不同的温度单位。它们虽然有着相同的底层类型 float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似 Celsius(t) 或 Fahrenheit(t) 形式的显式转型操作才能将 float64 转为对应的类型。

Celsius(t) 和 Fahrenheit(t) 是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。

类型转换

// 转换成 T 类型
t := T(x)
// 转换成 T 类型指针
t := (*T)(x)

对于每一个类型 T,都有一个对应的类型转换操作 T(x),用于将 x 转为 T 类型

在任何情况下,运行时不会发生转换失败的错误,错误只会发生在编译阶段

比较运算符 == 和 < 也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较

定义类型行为

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

许多类型都会定义一个 String 方法,因为当使用 fmt 包的打印方法时,将会优先使用该类型对应的 String 方法返回的结果打印

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%s\n", c) // "100°C"

数据类型

Go 语言内置以下这些基础类型

  • 布尔类型:bool
  • 整型:int8 、 byte、int16 、 int、uint 、 uintptr 等
  • 浮点类型:float32 、 float64
  • 复数类型:complex64 、 complex128
  • 字符串:string
  • 字符类型:rune
  • 错误类型:error

此外,Go 语言也支持以下这些复合类型:

  • 指针(pointer )
  • 数组(array )
  • 切片(slice )
  • 字典(map )
  • 通道(chan )
  • 结构体(struct )
  • 接口(interface )

整型

类型 长度 值范围
int8 1 -128 ~ 127
uint8(即 byte) 1 0 ~ 255
int16 2 -32768 ~ 32767
uint16 2 0 ~ 65535
int32 4 2147483648 ~ 2147483647
uint32 4 0 ~ 4294967295
int64 8 -9223372036854775808 ~ 9223 372036854775807
uint64 8 0 ~ 18446744073709551615
int 平台相关 平台相关
uint 平台相关 平台相关
uintptr 同指针 在 32 位平台下为 4 字节,64 位平台下为 8 字节

默认推导类型为int

需要注意的是,int 和 int32 在 Go 语言里被认为是两种不同的类型,编译器也不会帮你自动 做类型转换

两个不同类型的整型数不能直接比较,比如 int8 类型的数和 int 类型的数不能直接比较,但 各种类型的整型变量都可以直接与字面常量(literal )进行比较

浮点型

Go 语言定义了两个类型float32float64

默认推导类型为float64

复数类型

Go 语言定义了两个类型complex64complex128

默认推导类型为complex128

对于一个复数z = complex(x, y),就可以通过 Go 语言内置函数real(z)获得该复数的实部,也就是 x,通过imag(z)获得该复数的虚部,也就是 y。

字符串

常用操作

操作 描述 示例
x + y 字符串连接 “Hello” + “123” // 结果为 Hello123
len(s) 字符串字节长度 len(“Hello”) // 结果为 5
len([]rune(s)) 字符串字符长度 len([]rune(“ 世界 “)) // 结果为 2
s[i] 取字节(只读不能修改) “Hello” [1] // 结果为 ‘e’

字符串遍历

  1. 以字节数组的方式
str := "Hello, 世界"
n := len(str)
for i:=0; i<n; i++ {
ch:=str[i]
fmt.Println(i,ch)
}

这个例子的输出结果可以看出,这个字符串长度为 13。尽管从直观上来说,这个字符串应该只有 9 个字符。这是 因为每个中文字符在 UTF-8 中占 3 个字节,而不是 1 个字节。

  1. 以 unicode 字符遍历
str := "Hello, 世界"
for i,ch := range str { // 关键字range ,用于便捷地遍历容器中的元素
fmt.Println(i,ch) // ch的类型为 rune
}

以 unicode 字符方式遍历时,每个字符类型为 rune

字符类型

在 Go 语言中支持两个字符类型,一个是byte(实际上是 uint8 的别名),代表 UTF-8 字符串的单个字节的值;

另一个是rune,rune 的底层类型是 int32,代表单个 Unicode 字符。 出于简化语言的考虑,Go 语言的多数 API 都假设字符串为 UTF-8 编码。尽管 Unicode 字符在标 准库中有支持,但实际上较少使用。rune 的底层类型是 int32

数组

数组就是指一系列同一类型数据固定大小的顺序集合,数组中包含的每个数据被称为数组元素(element ),一个数组包含的元素个数被称为数组的长度

声明方式

var variable_name [SIZE] variable_type
// 创建数组并初始化
variable_name := [] variable_type{...}

在 Go 语言中,数组长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量表达式(常量表达式是指在编译期即可计算结果的表达式)。

在 Go 语言中数组是一个值类型(value type )。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。

数组切片

slice 是一种可以动态数组,可以按我们的希望增长和收缩。它的增长操作很容易使用,因为有内建的 append 方法。我们也可以通过 relice 操作化简 slice。因为 slice 的底层内存是连续分配的,所以 slice 的索引,迭代和垃圾回收性能都很好。

数组切片(slice )的数据结构可以抽象为以下 3 个变量:

  • 一个指向原生数组的指针
  • 数组切片中的元素个数
  • 数组切片已分配的存储空间

创建数组切片

  • 基于数组创建 Go 语言支持用myArray[first:last]这样的方式来基于数组生成一个数组切片
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基于数组创建一个myArray前5个元素创建数组切片
var mySlice []int = myArray[:5]
// myArray所有元素创建数组切片
mySlice = myArray[:]
// myArray第5个元素开始的所有元素创建数组切片
mySlice = myArray[5:]
  • 直接创建 Go 语言提供的内置函数make()可以用于 灵活地创建数组切片。当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。
// 创建一个初始元素个数为5的数组切片,元素初始值为0
mySlice1 := make([]int, 5)
// 创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
mySlice2 := make([]int, 5, 10)
// 直接创建并初始化包含5个元素的数组切片:
mySlice3 := []int{1, 2, 3, 4, 5}
  • 基于切片创建
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片

元素遍历

// 传统的元素遍历
for i := 0; i <len(mySlice); i++ {
fmt.Println("mySlice[", i, "] =", mySlice[i])
}
// 使用range关键字
for i, v := range mySlice {
fmt.Println("mySlice[", i, "] =", v)
}

动态增减元素

与数组相比,数组切片多了一个存储能力(capacity )的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能。

数组切片支持 Go 语言内置的 cap() 函数和 len() 函数,cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是 数组切片中当前所存储的元素个数

mySlice = append(mySlice, 1, 2, 3)
mySlice2 := []int{8, 9, 10} // 给mySlice后面添加另一个数组切片
mySlice = append(mySlice, mySlice2...)

数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间,数组切片会自动分配一块足够大的内存

内容复制 ( 值拷贝 )

数组切片支持 Go 语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}

copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

映射

映射(map )是一种无序的键值对的集合。map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。 map 是一种集合,所以我们可以像迭代数组和 slice 那样迭代它。不过,map 是无序的,我们无法决定它的返回顺序,这是因为 map 是使用 hash 表来实现的。

声明方式

// 通过字面值创建
map_variable := map[key_data_type]value_data_type{key:value}

// 通过 make 来创建
map_variable := make(map[key_data_type]value_data_type)

元素赋值

map_variable[key] = value

元素删除

delete(map_variable, key)

元素查找

value, isKeyExist := map_variable[key]

元素遍历

for key, value := range map_variable {
fmt.Printf("Key: %s Value: %s\n", key, value)
}

map 长度

len(map_variable)

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。

结构体声明

type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}

var dilbert Employee

结构体变量的成员可以通过点操作符访问 , 点操作符也可以和指向结构体的指针一起工作

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

// 相当于下面语句
(*employeeOfTheMonth).Position += " (proactive team player)"

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行

type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}

一个命名为 S 的结构体类型将不能再包含 S 类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是 S 类型的结构体可以包含 *S 指针类型的成员,这可以让我们创建递归的数据结构

结构体类型的零值是每个成员都对是零值。通常会将零值作为最合理的默认值。

结构体面值语法

type Point struct{ X, Y int }
// 以结构体成员定义的顺序为每个结构体成员指定一个面值
p := Point{1, 2}
// 以成员名字和相应的值来初始化,可以包含部分或全部的成员
p = Point{X:1,Y:2}

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在 Go 语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

结构体指针创建并初始化

pp := &Point{1, 2}

// 与上面等同
pp := new(Point)
*pp = Point{1, 2}

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用 == 或 != 运算符进行比较。相等比较运算符 == 将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:

type Point struct{ X, Y int }

p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q) // "false"

匿名成员

Go 语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

type Point struct {
X, Y int
}

type Circle struct {
Point
Radius int
}

得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

var c Circle
c.X = 8 // equivalent to c.Point.X = 8
c.Y = 8 // equivalent to c.Point.Y = 8
c.Radius = 5

匿名成员初始化

c := Circle{Point{8, 8}, 5}
// 或
c = Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
}

因为匿名成员也有一个隐式的名字(即类型名称),因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束。

Tag

在 Go 语言里,StructTag 是一个标记字符串,此字符串可跟随在 Struct 中字段定义的后面。StructTag 就是一系列的 key:"value" 形式的组合,其中 key 是一个不可为空的字符串,key-value 组合可以有多个,空格分隔。

StructTag 主要解决了不同类型数据集合间 (Struct,Json,Table 等 ) 转换中键值 Key 定义不一样的问题。StructTag 可以理解为一个不用数据类型键值 Key 的映射表 Map, 在 StructTag 中可以定义不用数据集合键值和 Struct 中 Key 值的映射关系,这样方便了 Struct 数据转为其他类型数据的过程。

package main
import (
"fmt"
"encoding/json"
)
type Person struct {
FirstName string `json:"first_name"` //FirstName <=> firest_name
LastName string `json:"last_name"`
MiddleName string `json:"middle_name,omitempty"`
}
func main() {
json_string := ` { "first_name": "John", "last_name": "Smith" }`
person := new(Person)
json.Unmarshal([]byte(json_string), person) //将json数据转为Person Struct
fmt.Println(person)
new_json, _ := json.Marshal(person) //将Person Sturct 转为json格式
fmt.Printf("%s\n", new_json)
}

// *Output*
// &{John Smith }
// {"first_name":"John","last_name":"Smith"}

运算符

数值运算符:

运算 含义
x + y
x - y
x * y
x / y
x % y 取模

比较运算符:

运 算 含 义
x < y 大于
x > y 小于
x >= y 大于等于
x <= y 小于等于
x == y 相等
x != y 不相等

位运算符 :

运 算 含 义
x << y 左移
x >> y 右移
x ^ y 异或
x & y
x ¦ y
^x 取反

流程控制

条件语句

 if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}

关于条件语句,需要注意以下几点:

  • 条件语句不需要使用括号将条件包含起来 ()
  • 无论语句体内有几条语句,花括号 {} 都是必须存在的
  • 左花括号 { 必须与 if 或者 else 处于同一行
  • 在 if 之后,条件语句之前,可以添加变量初始化语句,使用;间隔

选择语句

switch i {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3, 4:
fmt.Printf("3", "4")
default:
fmt.Printf("Default")
}

在使用 switch 结构时,我们需要注意以下几点:

  • 左花括号 { 必须与 switch 处于同一行
  • 条件表达式不限制为常量或者整数
  • 单个 case 中,可以出现多个结果选项
  • 与 C 语言等规则相反,Go 语言不需要用 break 来明确退出一个 case; 只有在 case 中明确添加fallthrough关键字,才会继续执行紧跟的下一个 case
  • 可以不设定 switch 之后的条件表达式,在此种情况下,整个 switch 结构与多个 if…else… 的逻辑作用等同。

循环语句

// 与C的for一样
for init; condition; post { }
// 与C的while一样
for condition { }
// 与C的while(true)一样
for { }

init: 一般为赋值表达式,给控制变量赋初值; condition : 关系表达式或逻辑表达式,循环控制条件; post : 一般为赋值表达式,给控制变量增量或减量。

使用循环语句时,需要注意的有以下几点 :

  • 左花括号 { 必须与 for 处于同一行。
  • Go 语言中的 for 循环与 C 语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go 语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
a := []int{1, 2, 3, 4, 5, 6}
for i, j := 0, len(a) – 1; i < j; i, j = i + 1, j – 1 {
a[i], a[j] = a[j], a[i]
}
  • Go 语言的 for 循环同样支持continuebreak来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
// break语句终止的是JLoop标签处的外层循环
break JLoop
}
fmt.Println(i)
}
}
JLoop:

跳转语句

goto 语句的语义非常简单,就是跳转到本函数内的某个标签

func myfunc() {
i := 0
HERE:
fmt.Println(i++)
if i < 10 {
goto HERE
}
}

函数

函数构成代码执行的逻辑结构。在 Go 语言中,函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句。

函数定义

func function_name( [parameter list] ) [(return_types)]
{
body of the function
}

大写字母开头的函数也是一样,相当于 class 中的带 public 关键词的公有函数;小写字母开头的就是有 private 关键词的私有函数。 Go 编程语言中的函数定义包括函数头和函数体。这里是一个函数的所有部分:

  • func 开始一个函数的声明。
  • 函数名称 (function_name):这是函数的名称。函数名和参数列表一起构成函数签名。
  • 参数:参数类似于占位符。 调用函数时,将一个值传递给参数。 此值称为实际参数或参数。 参数列表指的是函数的参数的类型,顺序和数量。 参数是可选的 ; 即,函数可以不用包含参数。
  • 返回类型:函数可以有返回值列表。return_types是函数返回的值的数据类型的列表。某些函数执行所需的操作,而不用 ( 无 ) 返回值。在这种情况下,return_type 就不是必需的。
  • 函数体:函数体包含定义函数的功能的语句集合。

不定参数

func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}

形如...type格式的类型只能作为函数的参数类型存在,并且必须是后一个参数。它是一个语法糖(syntactic sugar ),即这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会。 从内部实现机理上来说,类型 …type 本质上是一个数组切片,也就是 []type,这也是为 什么上面的参数 args 可以用 for 循环来获得每个传入的参数。

不定参数的传递

func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)

// 传递片段,实际上任意的int slice都可以传进去
myfunc3(args[1:]...)
}

任意类型的不定参数

将不定参数类型约束为 int,如果你希望传任意类型,可以指定类型为 interface{}

func Printf(format string, args ...interface{}) {
// ...
}

多返回值

Go 语言的函数或者成员的方法可以有多 个返回值,这个特性能够使我们写出比其他语言更优雅、更简洁的代码

func (file *File) Read(b []byte) (n int, err Error)

给返回值命名,就像函数的输入参数一样。 返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数的 return 语句时,会返回对应的返回值变量的值。 Go 语言并不需要强制命名返回值,但是命名后的返回值可以让代码更清晰,可读性更强, 同时也可以用于文档。 如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单地用一个下划线_来跳过这个返回值

匿名函数

f := func(x, y int) int {
return x + y
}

闭包

闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。

i:=0
a := func()(func()){
j := 0
return func(){
i++
j++
fmt.Printf("I:%d,J:%d\n",i,j)
}
}()
a()
a()

在变量 a 指向的闭包函数中,只有内部的匿名函数才能访问变量 i,而无法通过其他途径访问 到,因此保证了 i 的安全性。

包和文件

Go 语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以 .go 为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径 , 例如包 gopl.io/ch1/helloworld 对应的目录路径是 $GOPATH/src/gopl.io/ch1/helloworld

Go 语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件。

包声明

每个源文件都是以包的声明语句开始,用来指名包的名字。当包被导入的时候,包内的成员将通过类似包名 . 成员名的形式访问。

而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。

在每个源文件的包声明前仅跟着的注释是包注释。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释,如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释。如果包注释很大,通常会放到一个独立的 doc.go 文件中。

导入包

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在 import 语句中的导入路径也是字符串。

import (
"fmt"
"math/rand"
)

在 Go 语言程序中,每个包都是有一个全局唯一的导入路径。Go 语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用 Go 语言自带的 go 工具箱时,一个导入路径代表一个目录中的一个或多个 Go 源文件。

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同

关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。

  • 第一个例外,包对应一个可执行程序,也就是 main 包,这时候 main 包本身的导入路径是无关紧要的。名字为 main 的包是给 go build 构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。
  • 第二个例外,包所在的目录中可能有一些文件名是以 test.go 为后缀的 Go 源文件(译注:前面必须有其它的字符,因为以``前缀的源文件是被忽略的),并且这些源文件声明的包名也是以 _test 为后缀名的。这种目录可以包含两种包:一种普通包,加一种则是测试的外部扩展包。所有以 _test 为后缀包名的测试外部扩展包都由 go test 命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖,具体细节我们将在 11.2.4 节中介绍。
  • 第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如 “gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是 yaml。

导入的包之间可以通过添加空行来分组;通常将来自不同组织的包独自分组。包的导入顺序无关紧要,但是在每个分组中一般会根据字符串顺序排列。

如果我们想同时导入两个有着名字相同的包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。

import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)

包的匿名导入

如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的 init 初始化函数,这时候我们需要抑制 “unused import” 编译错误,我们可以用下划线来重命名导入的包。像往常一样,下划线为空白标识符,并不能被访问。

import _ "image/png" // register PNG decoder

面向对象编程

方法

一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

方法声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

package geometry

import "math"

type Point struct{ X, Y float64 }

// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

上面的代码里那个附加的参数 p,叫做方法的接收器 (receiver),早期的面向对象语言留下的遗产将调用一个方法称为 “ 向一个对象发送消息 ”。

在 Go 语言中,我们并不会像其它语言那样用 this 或者 self 作为接收器;我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母,比如这里使用了 Point 的首字母 p。

在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。下面是例子:

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call

在 Go 语言里,我们为一些简单的数值、字符串、slice 、 map 来定义一些附加行为很方便。方法可以被声明到任意类型,只要不是一个指针或者一个 interface。

// A Path is a journey connecting the points with straight lines.
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}

基于指针对象的方法

func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
// 调用
p := Point{1, 2}
(&p).ScaleBy(2)

// 或
p.ScaleBy(2) // 这种简写方法只适用于“变量”

编译器会隐式地帮我们用 &p 去调用 ScaleBy 这个方法。这种简写方法只适用于 “ 变量 ”

方法值和方法表达式

type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
// Call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}

方法可被变量接收,同一变量只可接收同一类型的方法(即同样的参数列表同样的返回类型)

接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

很多面向对象的语言都有相似的接口概念,但 Go 语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。

接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会展示出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

接口声明

package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 新的接口类型通过组合已经有的接口来定义
type ReadWriter interface {
Reader
Writer
}

上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名另一个接口,而不用声明它所有的方法。这种方式本称为接口内嵌。

实现接口的条件

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。

var w io.Writer
w = os.Stdout // OK: *os.File has Write method
var rw io.ReadWrite
rw = os.Stdout // OK: *os.File has Read, Write methods
// 规则甚至适用于等式右边本身也是一个接口类型
w = rwc

interface{} 被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}

每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在 Go 语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。当然也确实没有必要在具体的类型中指出这些共性。

接口值

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像 Go 语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。

在 Go 语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是 nil

var w io.Writer

w = os.Stdout

接口类型查询

判断一个能否转换为另一个接口

var rw io.ReadWrite = os.Stdout
if w,ok := rw.(io.Writer); ok{
w.Write([]byte("hello"))
}

查询接口指向的实例类型

var v1 interface{} ...

switch v:=v1.(type){
case int:
case string:
...
}

并发

并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Go 语言中的并发程序可以用两种手段来实现。支持 “ 顺序通信进程 ”(communicating sequential processes) 或被简称为 CSP。CSP 是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例 (goroutine) 中传递,尽管大多数情况下仍然是被限制在单一实例中。

协程

执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的进程(process )、进程内的线程(thread )以及进程内的协程(coroutine ,也叫轻量级线程)。与 传统的系统级线程和进程相比,协程的大优势在于其 “ 轻量级 ”,可以轻松创建上百万个而不 会导致系统资源衰竭,而线程和进程通常多也不能超过 1 万个。这也是协程也叫轻量级线程的 原因。

Goroutines

Go 语言在语言级别支持轻量级线程,叫 goroutine。Go 语言标准库提供的所有系统调用操作 (当然也包括所有同步 IO 操作),都会出让 CPU 给其他 goroutine。这让事情变得非常简单,让轻 量级线程的切换管理不依赖于系统的线程和进程,也不依赖于 CPU 的核心数量

在 Go 语言中,每一个并发的执行单元叫作一个 goroutine。

当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫它 main goroutine。新的 goroutine 会用 go 语句来创建。在语法上,go 语句是一个普通的函数或方法调用前加上关键字 go。go 语句会使其语句中的函数在一个新创建的 goroutine 中运行。而 go 语句本身会迅速地完成。

f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

Channel

channel 是 Go 语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或 多个 goroutine 之间传递消息。channel 是进程内的通信方式,因此通过 channel 传递对象的过程和调 用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用 分布式系统的方法来解决,比如使用 Socket 或者 HTTP 等通信协议。Go 语言对于网络方面也有非 常完善的支持。

channel 声明

和 map 类似,channel 也一个对应 make 创建的底层数据结构的引用。当我们复制一个 channel 或用于函数参数传递时,我们只是拷贝了一个 channel 引用,因此调用者何被调用者将引用同一个 channel 对象。和其它的引用类型一样,channel 的零值也是 nil。

var chanName chan ElementType

定义一个 channel 也很简单,直接使用内置的函数 make() 即可:

var ch chan int
// 或
ch := make(chan int)

一个 channel 有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个 goroutine 通过 channel 发送到另一个执行接收操作的 goroutine。发送和接收两个操作都是用 <- 运算符。在发送语句中,<- 运算符分割 channel 和要发送的值。在接收语句中,<- 运算符写在 channel 对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded

如果 channel 之前没有写入数据,那么从 channel 中读取数据也会导致程序阻塞,直到 channel 中被写入数据为止。

Channel 还支持 close 操作,用于关闭 channel,随后对基于该 channel 的任何发送操作都将导致 panic 异常。对一个已经被 close 过的 channel 之行接收操作依然可以接受到之前已经成功发送的数据;如果 channel 中已经没有数据的话讲产生一个零值的数据。

close(ch)

以最简单方式调用 make 函数创建的时一个无缓存的 channel,但是我们也可以指定第二个整形参数,对应 channel 的容量。如果 channel 的容量大于零,那么该 channel 就是带缓存的 channel。

缓冲机制

范创建的都是不带缓冲的 channel,这种做法对于传递单个数据的场景可以接受,给 channel 带上缓冲, 从而达到消息队列的效果。

chanName := make(chan ElementType, BufferSize)

即使没有读取方,写入方也可以一直往 channel 里写入,在缓冲区被 填完之前都不会阻塞。从带缓冲的 channel 中读取数据可以使用与常规非缓冲 channel 完全一致的方法,但我们也可 以使用 range 关键来实现更为简便的循环读取

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

一个基于无缓存 Channels 的发送操作将导致发送者 goroutine 阻塞,直到另一个 goroutine 在相同的 Channels 上执行接收操作,当发送的值通过 Channels 成功传输之后,两个 goroutine 可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者 goroutine 也将阻塞,直到有另一个 goroutine 在相同的 Channels 上执行发送操作。

基于无缓存 Channels 的发送和接收操作将导致两个 goroutine 做一次同步操作。因为这个原因,无缓存 Channels 有时候也被称为同步 Channels。当通过一个无缓存 Channels 发送数据时,接收者收到数据发生在唤醒发送者 goroutine 之前

关于无缓存或带缓存 channels 之间的选择,或者是带缓存 channels 的容量大小的选择,都可能影响程序的正确性。无缓存 channel 更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存 channel,这些操作是解耦的。同样,即使我们知道将要发送到一个 channel 的信息的数量上限,创建一个对应容量大小带缓存 channel 也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。

如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。

串联的 Channels(Pipeline )

Channels 也可以用于将多个 goroutine 链接在一起,一个 Channels 的输出作为下一个 Channels 的输入。这种串联的 Channels 就是所谓的管道(pipeline )。下面的程序用两个 channels 将三个 goroutine 串联起来

布尔值 ok,ture 表示成功从 channels 接收到值,false 表示 channels 已经被关闭并且里面没有值可接收。

func main() {
naturals := make(chan int)
squares := make(chan int)

// Counter
go func() {
for x := 0; x<1000000 ; x++ {
naturals <- x
}
close(naturals)
}()

// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * 1
}
close(squares)
}()

// Printer (in main goroutine)
for {
if y,ok := <-squares; ok{
fmt.Println(y)
}else {
break
}
}
}

// 或
func main() {
naturals := make(chan int)
squares := make(chan int)

// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()

// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()

// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}

单方向的 Channel

Go 语言的类型系统提供了单方向的 channel 类型,分别用于只发送或只接收的 channel。类型chan<- int表示一个只发送 int 的 channel,只能发送不能接收。相反,类型<-chan int表示一个只接收 int 的 channel,只能接收不能发送。(箭头 <- 和关键字 chan 的相对位置表明了 channel 的方向。)这种限制将在编译期检测。

因为关闭操作只用于断言不再向 channel 发送新的数据,所以只有在发送者所在的 goroutine 才会调用 close 函数,因此对一个只接收的 channel 调用 close 将是一个编译错误。

func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}

func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}

func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}

select

Go 语言直接在语言级别支持 select 关键字,用于处理异步 IO 问题。 select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。与 switch 语句可以选择任何可使用相等比较的条件相比,select 有比较多的 限制,其中大的一条限制就是每个 case 语句里必须是一个 IO 操作,大致的结构如下:

select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

超时机制

并发编程的通信过程中,需要处理的就是超时问题,即向 channel 写数据时发现 channel 已满,或者从 channel 试图读取数据时发现 channel 为空。如果不正确处理这些情况,很可能会导 致整个 goroutine 锁死。

// 首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1) go func() {
time.Sleep(1e9) // 等待1秒钟
timeout <- true
}()

// 然后我们把timeout这个channel利用起来
select {
case <-ch:
// 从ch中读取到数据
case <-timeout:
// 一直没有从ch中读取到数据,但从timeout中读取到了数据
}

这种写法看起来是一个小技巧,但却是在 Go 语言开发中避免 channel 通信超时的有效方法。 在实际的开发过程中,这种写法也需要被合理利用起来,从而有效地提高代码质量。

channel 的传递

在 Go 语言中 channel 本身也是一个原生类型,与 map 之类的类型地位一样,因此 channel 本身在定义后也可以通过 channel 来传递。

管道也是使用非常广泛 的一种设计模式,比如在处理数据时,我们可以采用管道设计,这样可以比较容易以插件的方式 增加数据的处理流程。

利用 channel 可被传递的特性来实现我们的管道。为了简化表达,我们假设在管道中传递的数据只是一个整型数,在实际的应用场景中这通常会是一个数据块。

type PipeData struct {
value int
handler func(int) int
next chan int
}

并行和并发

  • 两个队列,一个 Coffee 机器,那是并发
  • 两个队列,两个 Coffee 机器,那是并行
var quit chan int = make(chan int)

func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
quit <- 0
}


func main() {
// 开两个goroutine跑函数loop, loop函数负责打印10个数
go loop()
go loop()

for i := 0; i < 2; i++ {
<- quit
}
}

默认地, Go 所有的 goroutines 只能在一个线程里跑 。也就是说, 以上码都不是并行的,但是都是是并发的。(Go1.6 版本 , 当前 GOMAXPROCS 会被设置为可用的核数,之前默认为 1)

真正的并行 runtime.GOMAXPROCS

func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
quit <- 0
}


func main() {
runtime.GOMAXPROCS(2) // 最多使用2个核
// 开两个goroutine跑函数loop, loop函数负责打印10个数
go loop()
go loop()

for i := 0; i < 2; i++ {
<- quit
}
}

显式让出时间片 runtime.Gosched

func loop() {
for i := 0; i < 10; i++ {
runtime.Gosched() // 显式地让出CPU时间给其他goroutine
fmt.Printf("%d ", i)
}
quit <- 0
}


func main() {

go loop()
go loop()

for i := 0; i < 2; i++ {
<- quit
}
}

runtime 调度器

  • Gosched 让出 cpu
  • NumCPU 返回当前系统的 CPU 核数量
  • GOMAXPROCS 设置最大的可同时使用的 CPU 核数
  • Goexit 退出当前 goroutine( 但是 defer 语句会照常执行 )

同步锁

Go 语言包中的 sync 包提供了两种锁类型:sync.Mutexsync.RWMutex,前者是互斥锁,后者是读写锁。

var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}

对于从全局角度只需要运行一次的代码,比如全局初始化操作,Go 语言提供了一个 once 类型来保证全局的唯一性操作,如下:

var flag int32
var once sync.Once

func initialize() {
flag = 3
fmt.Println(flag)
}

func setup() {
once.Do(initialize)
}

func main() {
setup()
setup()
}

错误处理

error 接口

type error interface {
Error() string
}

对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将 error 作为多种返回值中的后一个,但这并非是强制要求:

func Foo(param int)(n int, err error) {
// ...
}

调用时的代码建议按如下方式处理错误情况:

n, err := Foo(0)

if err != nil {
// 错误处理
} else {
// 使用返回值n
}

defer

当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dstName)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile) }

即使其中的 Copy() 函数抛出异常,Go 仍然会保证 dstFile 和 srcFile 会被正常关闭。如果觉得一句话干不完清理的工作,也可以使用在 defer 后加一个匿名函数的做法:

defer func() {
// 做你复杂的清理工作
} ()

一个函数中可以存在多个 defer 语句,因此需要注意的是,defer 语句的调用是遵照先进后出的原则,即后一个 defer 语句将先被执行。

Panic 抛出异常

Go 语言引入了两个内置函数 panic() 和 recover() 以报告和处理运行时错误和程序中的错误场景:

func panic(interface{})
func recover() interface{}

一般而言,当 panic 异常发生时,程序会中断运行,并立即执行在该 goroutine 中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息。panic value 通常是某种错误信息。对于每个 goroutine,日志信息中都会有与之相对的,发生 panic 时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将 panic 异常和日志信息一并记录。

不是所有的 panic 异常都来自运行时,直接调用内置的 panic 函数也会引发 panic 异常;panic 函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用 panic。

switch s := suit(drawCard()); s {
case "Spades": // ...
case "Hearts": // ...
case "Diamonds": // ...
case "Clubs": // ...
default:
panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
}

虽然 Go 的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同。由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。

Recover 捕获异常

通常来说,不应该对 panic 异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。

如果在 deferred 函数中调用了内置函数 recover,并且定义该 defer 语句的函数发生了 panic 异常,recover 会使程序从 panic 中恢复,并返回 panic value。导致 panic 异常的函数不会继续运行,但能正常返回。在未发生 panic 时调用 recover,recover 会返回 nil。

func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}

反射

反射是由 reflect 包提供支持 . 它定义了两个重要的类型 , TypeValue.

Type

一个 Type 表示一个 Go 类型 . 它是一个接口 , 有许多方法来区分类型和检查它们的组件

函数 ` reflect.TypeOf

`interface{}`` 类型 , 并返回对应动态类型的 reflect.Type:

```go
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"

fmt.Printf 提供了一个简短的 %T 标志参数 , 内部使用 reflect.TypeOf 的结果输出

fmt.Printf("%T\n", 3) // "int"

Value

reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以持有一个任意类型的值 . 函数 reflect.ValueOf 接受任意的 interface{} 类型 , 并返回对应动态类型的 reflect.Value. 和 reflect.TypeOf 类似 , reflect.ValueOf 返回的结果也是对于具体的类型 , 但是 reflect.Value 也可以持有一个接口值 .

v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v) // "3"

和 reflect.Type 类似 , reflect.Value 也满足 fmt.Stringer 接口 , 但是除非 Value 持有的是字符串 , 否则 String 只是返回具体的类型 . 相同 , 使用 fmt 包的 %v 标志参数 , 将使用 reflect.Values 的结果格式化 .

fmt.Printf("%v\n", v)   // "3"

逆操作是调用 reflect.ValueOf 对应的 reflect.Value.Interface 方法 . 它返回一个 interface{} 类型表示 reflect.Value 对应类型的具体值 :

v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface() // an interface{}
i := x.(int) // an int
fmt.Printf("%d\n", i) // "3"
0%