1, 为了程序的可读性更强,Go 语言不允许导入某个包而不使用,在包名前加一个下划线可以让编译器接受这一类导入,并且调用对应包内的所有代码文件里定义的 init 函数。
2, 在 Go 语言中,标识符要么从包里公开,那么不从包里公开。当代码导入了一个包时,程序可以直接访问这个包中任意一个公开的标识符,这些标识符以大写字母开头,以小写字母开头的包不是公开的,不能被其他包中的代码直接访问。
3, 在查找 map 中的键时有两个选择,要么赋值给一个变量,要么为了精确查找,赋值给两个变量。赋值给两个变量时的一个值和复制给一个变量时的值一样,是 map 查询的结果值。如果指定了第二个值,就会返回一个布尔值,来表示查找的键是否存在于 map 中。
4, 定义变量的简短模式(使用 :=),简短模式的限制条件: 定义变量,同时显式初始化。不能提供数据类型。只能用在函数内部。简短模式并不总是重新定义变量,也可能是部分退化的赋值操作。退化赋值的前提条件是: 最少有一个新变量被定义,且必须是同一作用域。例如:
func main() {
x := 100
x, y := 200, 300 // 退化赋值操作正确
x := 200 // 错误,没有新变量被定义
{
x, y := 200, 300 // 错误,不在同一作用域
}
}
5, 定义常量使用 const 关键字,不曾使用的常量不会引发编译错误。 在常量组中如不指定类型和初始化操作,则与上一行非空常量优质( 表达式文本 )相同。 比如:
package main
import "fmt"
func main() {
const (
x unit 16 = 120
y // 与上一行 x 类型、右值相同
s = "abc"
z // 与 s 类型、右值相同
)
fmt.Printf("%T, %v\n", y, y)
fmt.Printf("%T, %v\n", z, z)
}
6, 除了常量、别名类型以及未命名类型外,Go 强制要求使用显示类型转换。同样不能将非 bool 类型结果当做 true/false 使用。
func main() {
x := 100
var b bool = x // 错误,x 为整数类型,不能赋值给布尔类型变量
}
7, 自增、自减不再是运算符。只能作为独立语句,不能用于表达式
func main() {
a := 1
++a // 语法错误,++ 只能置于变量名之后
if (a++) > 1 { // 语法错误,语句不能用于表达式
}
}
语句和表达式:
表达式通常是求值代码,可作为右值或参数使用。而语句完成一个行为,比如 if、for 代码块等。
表达式可以作为语句使用,但语句不能当做表达式。
8, 对符合类型( 数组、切片、字典、结构体 )变量初始化时,有一些语法限制:
初始化表达式必须含类型标签
左花括号必须在类型尾部,不能另起一行
多个成员初始值以逗号分隔
允许多行,但每行必须以逗号或右花括号结束
比如:
a := []int {
1,
2,
}
b := []int {1, 2}
9, 条件表达式值必须是布尔类型,可省略括号,且做花括号不能另起一行。
流控制语句 (if、switch等) 支持对初始化语句,可定义块局部变量或执行初始化函数。 比如:
func main() {
x := 10
if a, b := x+1, x+10; a<b { // 定义一个或多个局部变量( 也可以是函数返回值 )
}
}
10, switch 按从上到下、从左到右顺序匹配 case 执行。只有全部 case 语句全部匹配失败时,才会执行 default 块。
相邻的空 case 不构成多条件匹配
不能出现重复的 case 值
无需显式执行 break 语句,case 执行完毕之后自动中断。如需贯通后续 case,需执行 fallthrough,但不在匹配后续条件表达式。
package main
import "fmt"
func main () {
switch x := 5; x {
default :
fmt.Println(x)
case 5:
x += 10
fmt.Println(x)
fallthrough
case 6:
x += 20
fmt.Println(x)
}
}
11, 变参本质上就是一个切片。只能接受一道多个同类型的参数,且必须放在列表尾部。
func tets(s string, a...int) {
fmt.Printf("%T, %v\n", a, a) // 显示类型和值
}
func main() {
test("abc", 1, 2, 3, 4) // 输出为 []int, [1, 2, 3, 4]
}
将切片作为变参时,需进行展开操作。如果是数组,先将其转化为切片。
func test(a ...int) {
fmt.Println(a)
}
func main() {
a := [3]int{10, 20, 30}
test(a[:]...) // 输出为 [10 20 30]
}
12, 匿名函数是指没有定义名字符号的函数。
除没有名字之外,匿名函数和普通函数完全相同。最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。
匿名函数可直接调用,保存到变量,作为参数或返回值。
直接执行:
func main() {
func (s string) {
fmt.Println(s)
}("Hello World")
}
赋值给变量:
func main() {
add := func(x, y int) int {
return x + y
}
fmt.Println(add(1, 2))
}
作为参数:
func test(f func()) {
f()
}
func main() {
test(func() {
fmt.Println("Hello World!")
})
}
作为返回值:
func test() func(int, int) int {
return func(x, y int) int {
return x + y
}
}
func main() {
add := test()
fmt.Println(add(2, 3))
}
13, 语句 defer 向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用与资源释放、解除锁定、以及错误处理等操作。
def main() {
x, y := 1, 2
defer func(a int) {
fmt.Println("defer x, y =", a, y)
}(x)
x += 100
y += 200 // 输出为:
fmt.Println(x, y) // 101 202
} // defer function x, y = 1, 202
多个延迟调用函数按照 FILO 次序执行
func main() {
defer fmt.Println("last")
defer fmt.Println("first") // 输出结果为 first last
}
14, 错误处理,官方推荐的标准做法是返回 error 状态
package main
import (
"fmt"
"errors"
)
var errDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, errDivByZero
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err == errDivByZero {
log.Fatalln(err)
}
fmt.Println(z)
}
应该通过错误变量,而非文本内容来判定错误类型。
错误变量通常以 err 作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。
15, 定义不定大小的数组
data := [...]int {1, 2, 3, 4}
在定义多维数组时,只有第一维度允许使用 ...
data := [...][2]int {
{1, 2},
{3, 4}
}
16, Go 数组是值类型,赋值和传参操作都会复制整个数组数据。
func main() {
x := [2]int{1, 2}
func (arr[2]int) {
arr[0] = 100
fmt.Println(arr)
} (x)
fmt.Println(x)
}
如果需要,可改用指针或切片,以避免数据复制
func main() {
x := [2]int{1, 2}
func (arr*[2]int) {
arr[0] = 100
fmt.Println(*arr)
} (&x)
fmt.Println(x)
}
17, 映射类型不能直接修改 value 成员( 结构或数组 )
func main() {
type user struct {
name string
age byte
}
m := map[int]user {
1: {"Tom", 19},
}
m[1].age += 1 // 错误,字典类型不能直接修改 value
}
正确做法是返回整个 value, 等修改后在设置字典键值, 或直接用指针类型
type user struct {
name string
age byte
}
func main() {
m := map[int]user {
1: {"Tom", 19},
}
u := m[1]
u.age += 1
m[1] = u
fmt.Println(m[1])
m2 := map[int]*user {
1: &user{"Jack", 20}
}
m2[1].age ++
fmt.Println(*m2[1])
}
字典对象本身就是指针包装,传参时无需再次取地址
func test(x map[string]int) {
fmt.Printf("x: %p\n", x)
}
func main() {
m := make(map[string]int)
test(m)
fmt.Printf("m: %p, %d\n", m, unsafe.Sizeof(m))
m2 := map[string]int{}
test(m2)
fmt.Printf("m2: %p, %d\n", m2, unsafe.Sizeof(m2))
}
18, 可以使用指针直接操作结构字段,但不能是多级指针
func main() {
type user struct {
name string
age int
}
p := &user {
name: "Tom",
age: 20,
}
p.name = "Mary"
p.age ++
p2 := &p
*p2.name = "Jack" // 错误不能使用多级指针
}
字段标签并不是注释,而是用来对字段进行描述的元数据。尽管他不属于数据成员,但确实类型的组成部分。
type user struct {
name string `昵称`
sex byte `性别`
}
func main() {
u := user{"Tom", 1}
v := reflect.ValueOf(u)
t := v.Type()
for i, n := 0, t.NumField(); i<n; i++ {
fmt.Printf("%s: %v\n", t.Field(i).Tag, v.Field(i))
}
}
19, 方法是与对象实例绑定的特殊函数。
方法是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,
以属性和方法来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。
换句话说,方法是有关联状态的,而函数通常没有。
方法和函数定义的区别在于前者有前置实例接受参数( receiver ),编译器依次确定方法所属类型。
方法可以看做特殊的函数,那么 receiver 的类型当然可以是基础类型或指针类型。这关系到调用时对象实例是否被复制
type N int
func (n N) value() {
n++
fmt.Printf("v: %p, %v\n", &n, n)
}
func (n *N) pointer() {
(*n)++
fmt.Printf("v: %p, %v\n", n, *n)
}
func main() {
var a N = 25
a.value()
a.pointer()
}
选择方法的 receiver 类型:
1), 如果要修改实例状态,使用 *T
2), 无需修改状态的小对象或固定值,建议用 T
3), 大对象建议用 *T,以减少复制成本
4), 引用类型、字符串、函数等指针包装对象,直接用 T
5), 若包含 Mutex 等同步字段,用 *T,避免因复制造成锁操作无效
6), 其他无法确定的情况,都用 *T
20, 并发: 逻辑上具备同时处理多个任务的能力。
并行: 物理上在同一时刻执行多个并发任务。 ( 并行是并发设计的理想执行模式 )
使用 goroutine 实现并发任务:
只需在函数调用前添加 go 关键字即可创建并发任务。
关键字 go 并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取
执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行顺序。
与 defer 一样,goroutine 也会因 "延迟执行" 而立即计算并复制执行参数。
package main
import (
"fmt"
"time"
)
var c int
func counter() int {
c++
return c
}
func main() {
a := 100
go func(x, y int) {
time.Sleep(time.Second) // 让 goroutine 在 main 逻辑之后执行
fmt.Println("go:", x, y)
}(a, counter()) // 立即计算并复制参数
a += 100
fmt.Println("main:", a, counter())
time.Sleep(time.Second * 3) // 等待 goroutine 结束
}
进程退出时不会等待并发任务结束,可用通道 (channel) 阻塞,然后发出退出信号。
func main() {
exit := make(chan struct{}) // 创建通道,因为仅是通知,数据并没有实际意义
go func() {
time.Sleep(time.Second)
fmt.Println("goroutine done.")
close(exit) // 关闭通道,发出信号
}()
fmt.Println("main...")
<-exit // 如通道关闭,立即解除阻塞
fmt.Println("main exit.")
}
如果等待多个任务结束,推荐使用 sync.WaitGroup。通过设定计数器,让每个 goroutine 在退出前递减,直至归零时解除阻塞。
package main
import (
"fmt"
"time"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0;i<10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Second)
fmt.Println("goroutine", id, "done.")
}(i)
}
fmt.Println("main...")
wg.Wait()
fmt.Println("main exit.")
}
尽管 WaitGroup.Add 实现了原子操作,但建议在 goroutine 外累加计数器,以免 Add 尚未执行,Wait 已经退出。
func main() {
var wg sync.WaitGroup
go func() {
wg.Add(1) // 来不及设置
fmt.Println("hi!")
}()
wg.Wait()
fmt.Println("exit.")
}
可在多处使用 Wait 阻塞,他们都能接收到通知。
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
wg.Wait() // 等待归零,解除阻塞
fmt.Println("wait exit.")
}()
go func() {
time.Sleep(time.Second)
fmt.Println("done.")
wg.Done() // 递减计数
}()
wg.Wait() // 等待归零,解除阻塞
fmt.Println("main exit.")
}
21, 从底层实现上来说,通道只是一个队列。同步模式下,发送和接收双方配对,然后直接复制数据给对方。如配对失败,则置入等待队列,直到另一方出现后才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符 时,同样加入等待队列,知道有另一方写入数据或腾出空槽后被唤醒。
除了产出消息( 数据 )外,通道常还被用作事件通知。
func main() {
done := make(chan struct{}) // 结束事件
c := make(chan string) // 数据传输通道
go func() {
s := <- c // 接收消息
fmt.Println(s)
close(done) // 关闭通道
}()
c <- "hi!" // 发送消息
<- done // 阻塞,知道有数据或管道关闭
}
同步模式必须有配对操作的 goroutine 出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。
func main() {
c := make(chan int, 3) // 创建带 3 个缓冲槽的异步通道
c <- 1 // 缓冲区未满,不会阻塞
c <- 2
fmt.Println(<- c) // 缓冲区尚有数据,不会阻塞
fmt.Println(<- c)
}
内置函数 cap 和 len 返回缓冲区大小和当前已缓冲数量; 而对于同步通道则都返回 0,据此可判断通道是同步还是异步。
func main() {
a, b := make(chan int), make(chan int, 3)
b <- 1
b <- 2
fmt.Println("a:", len(a), cap(a)) // 0 0
fmt.Println("b:", len(b), cap(b)) // 2 3
}
除使用简单的发送和接收操作符外,还可用 ok-idom 或 range 模式处理数据。
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
if x, ok := <- c; ok {
fmt.Println(x)
} else {
return
}
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<- done
}
对于循环接收数据, range 模式更简洁一些。
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for x:= range c {
fmt.Pritnln(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<- done
}
默认通道是双向的,并不区分发送和接收端。但某些时候,我们可限制收发操作的方向来获得更严谨的操作逻辑。
通常使用类型转换来获取单向通道,并分别赋予操作双方。
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan <- int = c
var recv <- chan int = c
go func() {
defer wg.Done()
for x:= range recv {
fmt.Println(x)
}
}()
go func() {
defer wg.Done()
defer close(c)
for i := 0; i<3; i++ {
send <- i
}
}()
wg.Wait()
}
如果要同时处理多个通道,可选用 select 语句。它会随机选择一个可用通道做收发操作。
func main() {
var wg sync.WaitGroup
wg.Add(2)
a, b:= make(chan int), make(chan int)
go func() { // 接收端
defer wg.Done()
for {
var (
name string
x int
ok bool
)
select { // 随机选择可用 channel 接受数据
case x, ok = <- a:
name = "a"
case x, ok = <- b:
name = "b"
}
if !ok { // 如果任一通道关闭,则终止接收
return
}
fmt.Println(name, x) // 输出接受的信息数据
}
}()
go func() { // 发送端
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i<10; i++ {
select { // 随机选择发送 channel
case a <- i:
case b <- i * 10:
}
}
}()
wg.Wait()
}
如果要等到全部通道消息处理结束,可将已完成通道设置为 nil。这样它就会被阻塞,不在被 select 选中。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
for {
select {
case x, ok := <- a:
if !ok {
a = nil
break
}
fmt.Println("a", x)
case x, ok := <- b:
if !ok {
b = nil
break
}
fmt.Println("b", x)
}
if a == nil && b == nil {
return
}
}
}()
go func() {
defer wg.Done()
defer close(a)
for i:= 0; i<3; i++ {
a <- i
}
}()
go func() {
defer wg.Done()
defer close(b)
for i:=0; i<5; i++ {
b <- i*10
}
}()
wg.Wait()
}
即使是同一通道,也会随机选择 case 执行。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
go func() {
for i := 0; i<10; i++ {
select {
case c <- i:
case c <- i*10:
}
}
defer wg.Done()
defer close(c)
}()
go func() {
var (
data int
ok bool
)
for {
select {
case data, ok = <- c:
fmt.Println(data, ok)
case data, ok = <- c:
fmt.Println(ok, data)
}
if !ok {
break
}
}
defer wg.Done()
}()
wg.Wait()
}
22, 反射让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥补了静态语言在动态行为上的不足。同时反射还是元编程的
重要手段。在面对类型时,需要区分 Type 和 Kind。前者表示真是类型( 静态类型 ),后者表示其基础结构 ( 底层类型 )类别。
type X int
func main() {
var a X = 100
t := reflect.TypeOf(a)
fmt.Println(t.Name(), t.Kind()) // 输出 X int
}
传入对象应区分基础类型和指针类型,因为它们并不属于同一类型。
func main() {
x := 100
tx, tp := reflect.TypeOf(x), reflect.TypeOf(&x)
fmt.Println(tx, tp, tx == tp) // 输出为 int *int false
fmt.Println(tx.Kind(), tp.Kind()) // int ptr
fmt.Println(tx == tp.Elem()) // true
}
对于匿名字段,可用多级索引( 按定义顺序 ) 直接访问
type user struct {
name string
age int
}
type manager struct {
user
title string
}
func main() {
var m manager
t := reflect.TypeOf(m)
fmt.Println(t, t.Name(), t.Kind()) // 输出为 main.manager manager struct
name, _ := t.FieldByName("name")
fmt.Println(name.Name, name.Type) // 输出为 name string
age := t.FieldByIndex([]int{0,1})
fmt.Println(age.Name, age.Type) // 输出为 age int
}
和 Type 获取类型信息不同,Value 专注于对象实例数据读写。
func main() {
a := 100
va, vp := reflect.ValueOf(a), reflect.ValueOf(&a).Elem()
fmt.Println(va.CanAddr(), va.CanSet()) // false false
fmt.Println(vp.CanAddr(), vp.CanSet()) // true true
}
23, 数组、切片和映射:
数组:
在 Go 语言中,数组是一个值。这意味着数组可以用在赋值操作中。变量名代表整个数组。一年春,同样类型的数组可以赋值给另一个
数组,两个数组互不影响。
func main() {
a := [5]int {1,2 ,3, 4, 5}
b := a
fmt.Println(a, b) // [1 2 3 4 5] [1 2 3 4 5]
a[2] = 100
fmt.Println(a, b) // [1 2 100 4 5] [1 2 3 4 5]
}
复制指针数组,只会赋值指针的值,而不会赋值指针所指向的值。
func main() {
var array1 [3]*string
array2 := [3] *string{new(string), new(string), new(string)}
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
array1 = array2
*array1[1] = "changed"
fmt.Println(*array1[1], *array2[1]) // 输出 changed changed
}
切片:
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。
切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个
切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
创建和初始化:
1), make 和切片字面量
slice := make([]int, 3, 5) // 创建一个长度为 3,容量为 5 的切片对象
2),通过切片字面量声明切片
slice := []string{"red", "blue", "green"} // 创建一个长度和容量都是 3 的切片对象
3),使用索引声明切片
slice := []string{99: ""}
使用切片创建切片:
slice := []int {10, 20, 30, 40, 50}
newSlice := slice[1:3]
这两个切片对象共享同一段底层数组,通过不同的切片会看到底层数组的不同部分。第一个切片 slice 能够看到底层数组全部
5 个元素的容量,不过之后的 newSlice 就看不到。对于 newSlice,底层数组的容量只有四个元素。newSlice 无法访问到它所指向
的底层数组的第一个元素之前的部分。所以,对 newSlice 来说,之前的那些元素就是不存在的。
修改切片内容可能导致的结果:
slice := []int {10, 20, 30, 40, 50} // 创建一个长度和容量都是 5 的整形切片
newSlice := slice[1:3] // 创建一个长度是 2,容量是 4 的新切片
newSlice[1] = 35 // 修改 newSlice 索引为 1 的元素,同时也修改了原来的 slice 的索引为 2 的元素。
切片增长:
相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go 语言内置的 append 函数会处理增加长度时的所有操作
细节。append 调用返回时,会返回一个包含修改结果的新切片。函数 append 总是会增加新切片的长度,而容量有可能会改变,也
有可能不会改变,这取决于被操作的切片的可用容量。
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice = append(newSlice, 100) // 使用原有的容量来分配一个新元素。这会修改底层数组的值。
for i:=0; i<len(slice); i++ {
fmt.Println(i, slice[i]) // 底层数组的第四项被改变为 100.
}
24, 接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了
某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型和值值存入接口
类型的值。
接口是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫做 iTable,包含了所储存的值的类型信息。
iTable 包含了已储存的值的类型信息以及这个值相关联的一组方法。第二个字是一个指向所储存值的指针。将类型信息和指针组合在一起,
就将这两个值组成了一种特殊的关系。
重要!!
如果使用指针接受者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接受者来实现一个接口,
那么那个类型的值和指针都能够实现对应的接口。
package main
import (
"fmt"
)
// notifier 是一个定义了通知类行为的接口
type notifier interface {
notify()
}
// user 在程序里定义了一个用户类型
type user struct {
name string
email string
}
// notify 是使用指针接受者实现的方法
func (u *user) notify() {
fmt.Println("Sending user email to %s<%s>\n", u.name, u.email)
}
func main() {
u := user{"Bill", "bill@email.com"}
sendNotification(u) // 不能把 u (类型是 user) 作为 sendNotification 的参数
} // 改为 sendNotification(&u) 即可
func sendNotification(n notifier) {
n.notify()
}
通过接口实现多态的简单例子:
package main
import "fmt"
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u user) notify() {
fmt.Printf("Send user email to %s<%s>", u.name, u.email)
}
type admin struct {
name string
email string
}
func (a admin) notify() {
fmt.Printf("Send admin email to %s<%s>", a.name, a.email)
}
func main() {
bill := user{"Bill", "bill@email.com"}
sendNotification(bill)
lisa := admin{"Lisa", "lisa@email.com"}
sendNotification(lisa)
}
func sendNotification (n notifier) {
n.notify()
}
25, 嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型是被称为新的外部类型的内部类型。
通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,
也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以声明与
内部类型标识符同名的标识符来覆盖内部标识符的字段或方法。这就是扩展或者修改已有类型的方法。
package main
import "fmt"
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u *user) notify() {
fmt.Printf("Send user email to %s<%s>", u.name, u.email)
}
type admin struct {
user
level string
}
func (a *admin) notify() {
fmt.Printf("Send admain email to %s<%s>", a.name, a.email)
}
func main() {
ad := admin {
user: user {
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
sendNotification(&ad)
sendNotification(&ad.user)
}
func sendNotification(n notifier) {
n.notify()
}
26, 不同包中声明的公开和未公开的嵌套类型:
entities/entities.go
package entities
// user 在程序中定义了用户类型,是未公开的
type user struct {
Name string
Email string
}
type Admin struct {
user // 嵌入的类型是未公开的
Rights: int
}
由于内部类型 user 是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即使内部类型是未公开的,
内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。
package main
import "./entities"
func main() {
a := entities.Admin{Rights: 100}
a.Name = "bill"
a.Email = "bill@email.com"
}
27, Go 语言中的并发是指能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Go 会将其视为一个独立的工作
单元。这个单元会被调度到可用的逻辑处理器上执行。
Go 语言的并发同步模型来自一个叫做通信顺序进程 ( CSP ) 的范型。CSP 是一种消息传递模型,通过在 goroutine 之间传递
数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间传递数据的关键数据类型叫做通道。
如果创建一个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的
goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待到自己
被分配的逻辑处理器执行。
竞争状态:
如果有两个或多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的
状态,这种情况被称作竞争状态 ( rece candition )。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对
一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
// 这个实例程序展示如何在程序里造成竞争状态
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int // counter 是所有 goroutine 都要增加其值的变量
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
value := counter // 捕获 counter 的值
runtime.Gosched() // 当前 goroutine 从线程退出,并放回到队列
value++ // 增加本地 value 变量的值
counter = value // 将该值保存回 counter
}
}
锁住共享资源:
使用原子函数:
原子函数能够以很底层的加锁机制来同步访问整型变量和指针
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incCounter(id int) {
defer wg.Done()
for count:=0; count<2; count++ {
atomic.AddInt64(&counter, 1)
runtime.Gosched()
}
}
使用互斥锁:
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
mutex.Unlock()
}
}
28, 当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,
需要指定要被共享的数据的类型。可以通过通道共享内置类型、命名空间、结构类型和引用类型的值或者指针。
无缓冲的通道:
无缓冲的通道是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和 接收 goroutine 同时
准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送或者接收操作的 goroutine
阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
// 这个示例展示如何用无缓冲的通道来模拟 2 个 goroutine 之间的网球比赛
package main
import (
"fmt"
"sync"
"math/rand"
"time"
)
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
court := make(chan int)
wg.Add(2)
go player("Nadal", court)
go player("Djokovic", court)
court <- 1
wg.Wait()
}
func player(name string, court chan int) {
defer wg.Done()
for {
ball, ok := <- court
if !ok {
fmt.Printf("Player %s Won\n", name)
return
}
n := rand.Intn(100)
if n % 3 == 0 {
fmt.Printf("Player %s Missed\n", name)
close(court)
return
}
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
court <- ball
}
}
另一个例子,使用不同的模式,使用无缓冲的通道,在 goroutine 之间同步数据,来模拟接力比赛。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
baton := make(chan int)
wg.Add(1)
go Runner(baton)
baton <- 1
wg.Wait()
}
func Runner(baton chan int) {
var newRunner int
runner := <- baton
fmt.Printf("Runner %d Running With Baton\n", runner)
if runner != 4 {
newRunner = runner + 1
fmt.Printf("Runner %d To The Line\n", runner)
go Runner(baton)
}
time.Sleep(100 * time.Millisecond)
if runner == 4 {
fmt.Printf("Runner %d Finished, Race Over\n", runner)
wg.Done()
return
}
fmt.Printf("Runner %d Exchange With Runner %d\n", runner, newRunner)
baton <- newRunner
}
有缓冲的通道:
有缓冲的通道是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成
发送和接收。通道会阻塞发送和接收动作的条件也不同,只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用
缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间有一个很大的不同: 无缓冲的通道保证进行
发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const (
numberGoroutines = 4 // 要是用的 goroutine 的数量
taskLoad = 10 // 要处理的工作的数量
)
var wg sync.WaitGroup
// init 初始化包,Go 语言运行时会在其他代码执行之前优先执行这个函数
func init() {
rand.Seed(time.Now().Unix())
}
func main() {
// 创建一个有缓冲的通道来管理工作
tasks := make(chan string, taskLoad)
// 启动 goroutine 来处理工作
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}
// 增加一组要完成的工作
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Task : %d", post)
}
// 当所有工作都处理完时关闭通道以便所有 goroutine 退出
close(tasks)
// 等待所有工作完成
wg.Wait()
}
// worker 作为 goroutine 启动来处理从有缓冲的通道传入的工作
func worker(tasks chan string, worker int) {
// 通知函数已经返回
defer wg.Done()
for {
// 等待分配工作
task, ok := <- tasks
if !ok {
// 这意味着通道已经空了,并且已被关闭
fmt.Printf("Worker: %d : Shutting Down\n", worker)
return
}
// 显示我们开始工作了
fmt.Printf("Worker: %d : Started %s\n", worker, task)
// 随机等待一段时间来模拟工作
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep) * time.Millisecond)
// 显示我们完成了工作
fmt.Printf("Worker: %d : Completed %s\n", worker, task)
}
}
29, 标准库 log 包,记录日志的目的是跟踪程序什么时候在什么位置做了什么事。
// 这个实例程序展示如何使用最基本的 log 包
package main
import "log"
func init() {
// 定义函数名为 init()。这个函数会在运行 main() 之前作为程序初始化的一部分执行。通常程序会在这个函数
// 中配置日志参数,这样程序一开始就能使用 log 包进行正确的输出。
log.SetPrefix("TRACE: ")
// 设置了一个字符串,作为每个日志项的前缀。这个字符串是为了能让用户从一般的程序输出中分辨出日志的字符串。
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
func main() {
// Println 写到标准日志记录器
log.Println("message")
// Fatalln 在调用 Println() 之后会接着调用 os.Exit(1)
log.Fatalln("fatal message")
// Panicln 在调用 Println() 之后会接着调用 panic()
log.Panicln("panic message")
}
Ldate = 1 << iota
关键字 iota 在常量声明区里有特殊的作用。这个关键字让编译器为每个常量赋值相同的表达式,知道声明区结束或者遇到一个新的
赋值语句。关键字 iota 的另一个功能是,iota 的初始值为 0,之后 iota 的值在每次处理为常量后,都会自增 1 。
// 定制的日志记录器
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var (
Trace *log.Logger // 记录所有日志
Info *log.Logger // 重要的信息
Warning *log.Logger // 需要注意的信息
Error *log.Logger // 非常严重的问题
)
func init() {
file, err := os.OpenFile("errors.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Trace = log.New(ioutil.Discard, "TRACE:", log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout, "INFO:", log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(os.Stdout, "WARNING:", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR:", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("something has failed")
}
log.New 方法会创建一个新的 Logger,out 参数设置日志数据将被写入的目的地,prefix 参数会在生成的每行日志的最开始
出现,flag 参数定义日志记录包含哪些属性。
log.New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
30, 单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在指定的场景下,有没有按照期望工作。
一个场景是正向路径测试,就是在正常执行的情况下,保证不产生错误的测试。这种测试可以用来确认代码可以工作地向数据库中插入
一条工作记录。另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。
在 Go 语言中有几种方法写单元测试:
基础测试只使用一组参数和结果来测试一段代码
表组测试也会测试一段代码,但是会使用多组参数和结果进行测试。
也可以使用一些方法来模仿测试代码需要使用的外部资源。
基础单元测试:
// 这个示例程序展示如何写基础单元测试
package listing01
import (
"net/http"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
// TestDownload 确认 http 包的 Get 函数可以下载内容
func TestDownload(t *testing.T) {
url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
statusCode := 200
t.Log("Given the need to test downloading content.")
{
t.Logf("\tWhen checking \"%s\" for status code \"%d\"", url, statusCode)
{
resp, err := http.Get(url)
if err != nil {
t.Fatal("\t\tShould be able to make the Get call.", ballotX, err)
}
t.Log("\t\tShould be ablel to make the Get call.", checkMark)
defer resp.Body.Close()
if resp.StatusCode == statusCode {
t.Logf("\t\tShould receive a \"%d\" status. %v", statusCode, checkMark)
} else {
t.Errorf("\t\tShould receive a\"%d\" status. %v %v", statusCode, ballotX, resp.StatusCode)
}
}
}
}
一个测试函数必须是公开的函数,并且以 Test 单词开头。不但函数名字要以 Test 开头,而且函数的签名必须接受一个指向
testing.T 类型的指针,并且不返回任何值。如果没有遵守这些约定,测试框架就不会认为这个函数时一个测试函数,也不会让
测试工具去执行它。
表组测试,如果测试可以接受一组不同的输入并产生不同的输出的代码,那么应该使用表组测试的方法进行测试。表组测试除了
会有一组不同的输入值和期望结果之外,其余部分都很像基础单元测试。
// 这个示例程序展示如何写一个基本的表组测试
package listing08
import (
"net/http"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
// TestDownload 确认 http 包的 Get 函数可以下载内容并处理不同的状态
func TestDownload(t *testing.T) {
var urls = []struct {
url string
statusCode int
} {
{
"http://www.goinggo.net/feeds/posts/default?alt=rss",
http.StatusOK,
},
{
"http://rss.cnn.com/rss/cnn_toopstbadurl.rss",
http.StatusNotFound,
},
}
t.Log("Given the need to test downloading different content.")
{
for _, u := range urls {
t.Logf("\tWhen checking \"%s\" for status code \"%d\"", u.url, u.statusCode)
{
resp, err := http.Get(u.url)
if err != nil {
t.Fatal("\t\tShould gbe able to Get the url.", ballotX, err)
}
t.Log("\t\tShould be able to Get the url.", checkMark)
defer resp.Body.Close()
if resp.StatusCode == u.statusCode {
t.Logf("\t\tShould have a \"%d\" status. %v", u.statusCode, checkMark)
} else {
t.Errorf("\t\tShould have a \"%d\" status %v %v", u.statusCode, ballotX, resp.StatusCode)
}
}
}
}
}
模仿调用:
之前的单元测试和表组测试都需要访问互联网,才能保证测试运行成功,如果没有了互联网连接,运行基础单元测试会测试
失败。为了修正这个问题,标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于 HTTP 的网络调用。模仿是
一个很常用的技术手段,用来运行测试时模拟访问不可用的资源。
// 这个示例程序展示如何内部模仿 HTTP GET 调用,与之前的例子有些差别
package listing12
import (
// "encoding/xml"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
// feed 模仿了我们期望接受的 XML 文档
var feed = `<?xml version="1.0" encoding="UTF-8"?>
<rss>
<channel>
<title>Going Go Pragramming</title>
<description>Golang : https://github.com/goinggo</description>
<link>http:www.goinggo.net/</link>
<item>
<pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
<title>Object Oriented Programming Mechanics</title>
<description>Go is an object oriented language.</description>
<link>http://www.goinggo.net/2015/03/object-oriented</link>
</item>
</channel>
</rss>`
// mockServer 返回用来处理请求的服务器的指针
func mockServer() *httptest.Server {
f := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/xml")
fmt.Fprintln(w, feed)
}
return httptest.NewServer(http.HandlerFunc(f))
}
// TestDownload 确认 http 包的 Get 函数可以下载内容并且内容可以被正确地反序列化并关闭
func TestDownload(t *testing.T) {
statusCode := http.StatusOK
server := mockServer()
defer server.Close()
t.Log("Given the need to test downloading content.")
{
t.Logf("\tWhen checking \"%s\" for status code \"%d\"", server.URL, statusCode)
{
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal("\t\tShould be able to make the Get call.", ballotX, err)
}
t.Log("\t\tShould be able to make the Get call.", checkMark)
defer resp.Body.Close()
if resp.StatusCode != statusCode {
t.Fatalf("\t\tShould receive a \"%d\" status. %v %v", statusCode, ballotX, resp.StatusCode)
}
t.Logf("\t\tShould receive a \"%d\" status. %v", statusCode, checkMark)
}
}
}