Go学习笔记

邹业盛 2021-11-21 20:11 更新
  1. 安装
  2. Hello World
  3. 变量声明与赋值
  4. 数据类型
  5. 流程控制和操作符
  6. 错误和异常
  7. goroutine, 通道, 并发
  8. 指针,空间分配和存续
  9. 链表
  10. 字符与字节
  11. 文件IO
  12. 命令行参数
  13. 日志
  14. 测试
  15. 并发与锁
  16. 项目模块,包,名字空间
  17. 最后自己的一点看法

1. 安装

新版本的 Go 编译器,是使用 go 自己实现的,不是用 C ,所以直接下载源码编译不了,一般也不用折腾了。可以直接到官网下载二进制可执行文件,比如 https://golang.org/dl/go1.17.3.linux-amd64.tar.gz 。解压后使用 bin 里面的 go 命令即可。

过程中,要使用第三方的包,或者要找官方包的文档,可以在 https://pkg.go.dev/ 搜索。(这里的“包”,说成是“模块”更合适)

2. Hello World

创建一个 demo.go 文件:

package main

func main() {
    println("Hello World!")
}

注意两点,要直接可执行,需要有 main 包,它里面要有 main 函数。

之后可以直接:

go run demo.go

也可以:

go build demo.go
./demo

-o 参数可以指定编译出的文件名。

go build -o a demo.go
./a

3. 变量声明与赋值

go 有不同的数据类型,变量声明的类型与数据类型需要匹配。

var text string
text = "Hello World!"

这样可以,也可以直接:

var text = "hello world"

go 会自动推断类型,知道 text 类型是 string

还有一种声明方式,涉及“重声明”机制:

先看:

var text = "1"
var text = "2"

这样写,不能通过编译,因为 text 已经声明过。你可以重新赋值,但是不能重新声明:

var text = "1"
text = "2"

本来声明和赋值是很分得很清楚的,但是当赋值的能力涉及到“模式匹配”,或者说“多值赋值”(我现在还不知道 go 有没有模式匹配能力)时,问题就麻烦一点了。

var a, b = "1", "2"
a, b = "3", "3"

如果多值当中,有新声明的变量要“声明且赋值”,有旧的变量要“重新赋值”,那怎么办?

var a, b = "1", "2"
var c, b = "3", "3"

语法上选择了 a, b 可以省略多余的 var ,那么第二行就不能解释成只新声明 c ,不声明 b 了。

也许是为了解决这种状况, go 搞了一个 := 出来:

func main() {
    var a, b = "1", "2"
    c, b := "3", "3"
    e := "s"
    fmt.Printf("%v %v %v %v\n", a, b, c, e)
}

:= 的规则就是,左侧至少有一个新声明的变量,这样,它就会自动对旧变量作不声明,只重赋值。嗯,感觉是在给编译器擦屁股。

4. 数据类型

4.1. 基本静态类型

基本静态类型,个人把它们看成四大类:

说具体类型之前,先介绍一个工具, unsafe.Sizeof ,它可以输出变量所占的“字节数”。(有些变量是“引用类型”的,所以它的值发生的变化时本身所占的字节数并不会变)

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var b bool
    b = true
    fmt.Printf("%v\n", unsafe.Sizeof(b))
}

能看到 1 的输出,所以 布尔型的数据,会占 1 个 Byte ,即 8 Bits 。

布尔型,整数,浮点,都比较简单,直接列出下面的表格就行:

类型名 名称 占用字节 范围
bool 布尔量 1 true 和 false
uint8, uint16, uint32, uint64 无符号整数 1,2,4,8 255, 65535, 42亿, MAX
int8, int16, int32, int64 有符号整数 1,2,4,8 127, 32767, 21亿, MAX
float32, float64 有符号浮点 4,8 MAX

另外还有一个 int 类型,会因为操作系统的位数不同,而使用 int32 或者 int64 。在我的机器上是 int64 ,占 8 个字节。

4.2. 类型别名

使用 type ,可以指定一个自定义的类型名字:

type myInt int32
var n myInt

4.3. 字节,字符,字符串

go 中,仍然有双引号表示“字符串”,单引号表示“字符”的传统方法。不过,这里说的单引号的“字符”指的是 Unicode 真正的字符抽象表示(不是字节那种具体形式),占 4 个字节。

func main() {
    var b  = 'a'
    var c  =  b + 1
    var d  =  '\xFF'
    var e = "abcdefghikk918298371jkjfkhh832"
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b)
    fmt.Printf("%v\n", c)
    fmt.Printf("%v\n", d)
    fmt.Printf("%v\n", unsafe.Sizeof(e))
    fmt.Printf("%v\n", e)
}

上面的例子,可以看出:

rune 是字符, go 中自然有对应的“字节”类型,就是 byte

func main() {
    var b byte
    b = 'a'
    var c  =  b + 1
    var d  =  '\xFF'
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b)
    fmt.Printf("%v\n", c)
    fmt.Printf("%v\n", d)
}

直接把 b 声明成 byte 之后,上面的代码都是完全没问题的, byte 本身是 uint8 (这里有一隐式的类型转换)。

对于 rune ,它的数字值是字符对应的 Unicode 编码的直接数字表达值:

func main() {
    var b rune
    b  = '邹'
    var c  =  b + 1
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b)
    fmt.Printf("%v\n", c)
}

的 Unicode 是 \x90B9 ,直接看成数字就是 37049 。

简单来说,就是对于 byterune ,都可以直接用数字来处理。

字符串我现在搞不懂,暂时把它当成内置对象看了。

4.4. 数组与切片

其实我一直“数组”的这个名字比较纠结,因为它里面不一定是“数字”啊,所以一般我喜欢叫它们“列表”。

go 中,列表是一种静态类型,即大小固定,值传递。

func main() {
    var b [3]bool
    b = [3]bool{true, true}
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b[0])
}

这种写法下,需要把 [3]bool 整体看成是一种类型,而不是只看 []bool

在 go 中, []bool 这类长度留空的“列表”,不是列表类型,而是“切片”类型!( slicearray 或者说 list 不是一回事)

func main() {
    type myBoolList [3]bool
    var b myBoolList
    b = myBoolList{true, true}
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b[0])
}

这样处理是正确的。

func main() {
    type myBoolList []bool
    var b myBoolList
    b = myBoolList{true, true}
    fmt.Printf("%v\n", unsafe.Sizeof(b))
    fmt.Printf("%v\n", b[0])
}

这样处理也正确,但是你会看到 []bool 的对象,会占用 24 个字节,所以“切片”已经不是一种静态结构了。

如果要实现“自动分配固定长度”的列表,需要使用 [...] 语法:

var b = [...]bool{true, true}
fmt.Printf("%v\n", unsafe.Sizeof(b))
fmt.Printf("%v\n", b[0])

这样, b 就只占 2 个字节。但是不能使用:

type myBoolList [...]bool

这种 myBoolList 算啥?没法解释。

“列表”和“切片”都支持切片操作。

func main() {
    var b = [...]int32{1,2,3,4}
    var a = b[1:2]
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a[0])
}

区间取值是前闭后开,前后都可以省略:

var a = b[:]

不过不支持负值索引。

使用 len() 函数可以获取数组和切片的长度(注意,数组是定长,它的长度不一定等于成员个数):

var l = [5]int{1,2,3}
println(len(l))
var l = []int{1,2,3}
println(len(l))
var l = [...]int{1,2,3}
println(len(l))

切片可变化,使用 append() 可以在尾部添加成员,并返回新的引用:

var l = []int{1,2,3}
println(len(l))
l = append(l, 2, 3, 4)
println(len(l))

4.5. 映射

映射,也叫字典, map 。这种类型,或者说这种对象, go 的封装程度比较高:

func main() {
    var a map[int32]int32
    a = map[int32]int32{}
    a[1] = 123
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a[1])
}

声明时,通过直观的语法声明 keyvalue 的类型即可。

还可以声明和赋值一起:

var a = map[int32]int32{1: 123}
var b = map[string]int32{"1": 123}

个人猜测任何 hashable 的对象都可以作为 key ,后面验证一下。

获取一个不存在的 key ,不会引发错误:

var m = map[string]string{}
m["a"] = "123"
var s string = m["b"]
println(s == "")

没有办法直接判断 key 是否存在,只能取值,通过第二个返回值判断:

var m = map[string]string{}
m["a"] = "123"
s, ok := m["b"]
println(s == "")
println(ok)

使用 delete 函数删除 key

var m = map[string]string{}
var s string
var ok bool

m["a"] = "123"
s, ok = m["a"]
println(s == "")
println(ok)
delete(m, "a")
s, ok = m["a"]
println(s == "")
println(ok)

4.6. 结构体与函数

和 C 一样,通过 type 可以声明一个类型的“别名”,也可以直接通过 type 声明新的类型:

func main() {
    type point struct {
        x int32
        y int32
    }
    var a = point{x: 1, y: 2}
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a.x)
}

可以:

var a point
a.x = 1; a.y = 2

但是不可以:

var a point = {x: 1, y: 2}

还可以使用 new ,像指针那样操作:

var a = new(point)
a.x = 1; a.y = 2
fmt.Printf("%v\n", (*a).y)

这里把“函数”和结构体一起讲,是希望突出一个关键的语言特性,即我们常看到的一种说法——“函数是一等公民”。

在 go 中,函数是一种基本类型,可以被用于成员定义,参数传入,返回。

func main() {
    type point struct {
        x int32
        y int32
        add func(int32, int32) int32
    }
    var a point
    a.x = 1; a.y = 2
    a.add = func(a int32, b int32) int32 {
        return a + b
    }
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a.add(a.x, a.y))
}

pointadd 成员,是一个函数,函数签名是两个 int32 的参数,返回 int32

实际的使用中,我们可以直接给 a.add 赋值一个匿名函数。

4.7. 函数与可变参数

上一部分说到了函数是基本类型,现在细看一下函数的行为。

作为类型声明:

func main() {
    type onePFunc func(int32) string
    var a onePFunc = func(a int32) string {
        return "hello"
    }
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a(1))
}

这里的 a 参数即使没有用到,编译器也不会提醒的。但是某个 import 的东西没有用到,却会提醒。(个人很烦这个限制)

作为结构体成员:

func main() {
    type hasFunc struct {
        f func(int32) string
    }
    var a = hasFunc{f: func(a int32) string {return "hello"}}
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a.f(1))
}

作为参数:

func main() {
    var a = func(f func(int32) int32) int32{
        return f(1)
    }
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a(func(n int32) int32 { return n+1}))
}

作为函数返回:

func main() {
    var a = func(n int32) func() int32{
        return func() int32 {
            return n + 1
        }
    }
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a(2)())
}

立即执行:

func main() {
    var a = 123;
    (func(){
        a = 456
    })()
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a)
}

go 中处理函数参数可变的方式,是使用可变参数,而不是使用“同名但是签名不同”。但是我没有找到返回可变类型的办法,如果需要返回的类型可变,那只能通过“接口”之类的机制再做一层抽象。

可变参数在 go 中的处理方式,是对于同种类型,最后一个参数,统一作为一个“切片”:

func main() {
    var a = func(a int32, b ...int32) int32 {
        return a + b[1]
    };
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a(1, 2, 3, 4))
}

4.8. 接口

go 中没有“类”,但是它有“接口”的机制。对应地,“接口的实现”变成放到 struct 中去做。

还有,接口不能写在函数中。

package main

import (
    "fmt"
    "unsafe"
)

type Runable interface {
    run(string) string
}

type Person struct {
    name string
}

func (p Person) run(name string) string {
    return p.name + ":" + name + ":" + "running"
}

func main() {

    var a = Person{name: "abc"}

    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", a.run("haha"))
}

上面代码中:

func (p Person) run(name string) string {
    return p.name + ":" + name + ":" + "running"
}

这个语法在个人看来很丑。它的作用是给 Person 这种类型添加了一个 run() 的方法。

“方法”本身和“接口”没有必然的联系,上面的代码中即使不定义 Runable 的接口,也不影响 Person 类型多一个 run() 方法。

还有, Person 的类型中,它的成员可以直接有一个名为 run 的函数:

type Person struct {
    name string
    run func(string) string
}

这种情况下, Person 算不算是实现了 Runable 接口呢?(对 go 来说不算)

个人观点,struct 本身应该是一个完整的结构,但它的方法却要和其它一些成员分开来写,这太别扭了。

还有,从上面的流程看,“接口”除了在编译期做一些检查,在运行期是完全没必要存在的。不知道 go 中怎么处理动态接口,或者一些运行时加载的功能怎么处理。

接口定义之后,就可以面向接口做函数的签名了:

func main() {

    var a = Person{name: "abc"}
    var f = func(r Runable) string {
        return r.run("waa")
    }

    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", f(a))
}

顺便可以检查一下,对于:

type Person struct {
    name string
    run func(string) string
}

这种,即使给了 run 的一个实现,编译器也不认为 Person 实现了 Runable 接口:

package main

import (
    "fmt"
    "unsafe"
)

type Runable interface {
    run(string) string
}

type Person struct {
    name string
    run func(string) string
}


func main() {

    var a = Person{name: "abc", run: func(name string) string {return name + " is running"}}
    var f = func(r Runable) string {
        return r.run("waa")
    }

    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v\n", f(a))
}

编译会报错,提示说 Person.run 是一个 field ,不是一个 method

同时,你也不能给 int 添加一个方法:

func (i int) run() string {
    return "running"
}

报的错是,不能给一个 non-local 类型添加新的方法。

4.9. 结构体,值使用和指针使用

我们可以以“值”,或者以“指针”来使用结构体,它们之间是参数传递的东西不同的区别:

package main

import "fmt"

type Add interface {
    add(int, int) int
    add2(int, int) int
}

type point struct {
    x int32
    y int32
}

func (p point) add(a int, b int) int {
    p.x = 9
    return a + b
}

func (p *point) add2(a int, b int) int {
    p.x = 9
    return a + b
}

func main() {
    var p point = point{}
    p.x = 1; p.y = 2
    p.add(2, 3)
    fmt.Println(p)

    var p2 *point = &point{}
    p2.x = 1; p2.y = 2
    p2.add2(2, 3)
    fmt.Println(p2)
}

上面示例中, add2 因为使用了指针,所以可以把 p2x 改了。而 add 因为是直接使用的值,传递时已经复制了对象,所以改的 p.x 已经不是 main() 中的 p 了。

当然,这里接口的实现,跟 pp2 使用两种不同的初始化方式没有关系。在这里,不同的初始化方式,只是单纯为了演示。

4.10. 类型断言,类型转换

go 专门有一种后置的“类型断言”语法,用于通用的 interface{} 往特定的类型转换(基本类型,像整数,字符串,有专门的函数做转换):

package main

import "fmt"

type Point struct {
    x int32
    y int32
}

type Location struct {
    x int32
    y int32
    z int32
}

func getType() interface{} {
    return Point{x: 1, y: 2}
}

func getType2() interface{} {
    return &Point{x: 1, y: 2}
}

func getType3() interface{} {
    return Location{x: 1, y: 2}
}


func main() {
    var p0 = getType()
    var p1 interface{} = getType()
    var p2 Point = getType().(Point)
    var p3 *Point = getType2().(*Point)
    var p4 interface{} = getType3()
    var p5 = p4.(Location)
    fmt.Println(p0, p1, p2, p3, p4, p5)

    switch value := p4.(type) {
    case Location:
        fmt.Println("Location", value)
    case Point:
        fmt.Println("Point", value)
    }
}

通过 p4.(Location) 来完成类型的转换(具象化)。

5. 流程控制和操作符

go 的流程控制语句很简单,一共只有:

还专门提供了 goto ,嗯,很暴力。

5.1. if

if 后面不用加括号:

func main() {
    if true {
        println("hello")
    }
}

if 后面强制需要 bool 类型,给个 1 是不行的。同时,各种类型的转换,也是使用特定的函数,比较死板。

运算符没有什么特别的,“与否非”分别是 &&||!

func main() {
    if (1 > 1) && (2 > 0) {
        println("hello")
    } else {
        println("world")
    }
}

5.2. for

for 是一个通用的迭代实现,可以看成是流程控制,同时兼具传统的 whilefor 的作用:

传统 for 的形式是典型的三段:

for i := 0; i < 10; i++ {
    println(i)
}

for 里面的变量声明及赋值,只能用 :=

三段可以任意省略。

全省,就是一个死循环:

var count int = 0;
for {
    println(count)
    count++
    if count == 10 { break }
}

for 后面只跟一个表达式,则它的行为跟传统的 while 一样:

var count int = 0;
for count < 10 {
    println(count)
    count++
}

再来看 for 的迭代表现。这里,感觉更像在语法层面给 forrange 开的后门。

是的, range 是一个 statement ,但同时,又可以像函数那么用它(有点像 Python2.x 的 print)。

var l = [3]int{2,3,4}
for a, n := range l {
    fmt.Printf("%v\n", unsafe.Sizeof(a))
    fmt.Printf("%v: %v\n", a, n)
}

遍历“列表”,两个值,一个是索引,另一个是列表成员。

“切片”的行为同“列表”一样。

如果是 map 的话,则会迭代 keyvlaue

func main() {
    var l = map[string]int{"a": 3, "b": 9, "c": 4}
    for a, n := range l {
        fmt.Printf("%v\n", unsafe.Sizeof(a))
        fmt.Printf("%v: %v\n", a, n)
    }

}

对于 string 则是字符索引和字符值(整数):

func main() {
    var l = "hello"
    for a, n := range l {
        fmt.Printf("%v\n", unsafe.Sizeof(a))
        fmt.Printf("%v: %v\n", a, n)
    }

}

range 还可以持续读取一个通道,行为像生成器:

package main

import "fmt"
import "time"


func gen() chan int {
    var ch = make(chan int)
    var i = 0
    go func(){
        for {
            i += 1
            ch <- i
            time.Sleep(1 * time.Second)
        }
    }()
    return ch
}

func main() {
    for i := range gen() {
        fmt.Println(i)
    }
}

5.3. switch

switch 是结构化的多路 if ,我在想,在 map 简单好用的情况下它会有多少的出场率。

可以针对一个变量:

func main() {
    var l = 3;
    switch l {
        case 1:
            println("1")
        case 2:
            println("2")
        default:
            println("other")
    }
}

针对单个变量时也可以做多值判断:

func main() {
    var l = 1;
    switch l {
        case 1,2,3:
            println("2")
        default:
            println("other")
    }
}

可以把逻辑表达式放在 case 中:

func main() {
    var l = 3;
    switch {
        case l == 1:
            println("1")
        case l == 2:
            println("2")
        default:
            println("other")
    }
}

可以通过 fallthrough 实现 next 的功能,同时会跳过 case 的逻辑判断:

func main() {
    var l = 1;
    switch {
        case l == 1:
            println("1")
            fallthrough
        case l == 2:
            println("2")
            break
        case l == 3:
            println("3")
        default:
            println("other")
    }
}

5.4. goto

func main() {
    var l = [3]int{1,2,3}


    for _, i := range l {
        if i == 3 {
            goto exit
        }
        println(i)
    }

    exit:
    println("exit")
}

注意一下 label 的语法,以 : 结尾。在行前加一个 label 对它之后的语句并没有额外影响。

6. 错误和异常

6.1. 错误

go 中官方提供了专门的 errors 工具包,和 error 接口:

type error interface {
    Error() string
}

所以,错误本身,并没有什么特别,用它或者不用它,不影响你写代码:

package main

import (
    "errors"
)


func getError() error {
    return errors.New("this is a error")
}

type Cls struct {
    name string
}

func (c Cls) Error() string {
    return c.name
}

func main() {
    var e error = getError()
    var c Cls = Cls{name: "haha"}
    println(e.Error())
    println(c.Error())
}

6.2. 异常 panic, recover, defer

go 中的异常,不像传统的 throw catch 那套,至少名字上不是。

看一个异常的例子:

package main

func div(i int) int {
    return 10 / i
}

func main() {
    var i int = div(0)
    println(i)
}

说到异常,最先想到的就是 N / 0 (当然,js 中有 Infinity 或者 NaN 人家就是不用异常) ,但是,不能直接写 1 / 0 ,这种编译器还是可以发现的,得用函数包装一下编译器就不和你墨迹了。

成功编译,在执行的时候,就能看到运行时报错,及调用栈。

要捕捉异常,在 go 中使用 recover() 函数,是的,一个函数。但是现在问题变成,这个函数如果放在前面,那么调用它时还没有异常。如果放在后面,因为异常中断了程序,无法调用。

因此, go 搞了一个 defer 语句,来把指定的语句推到当前函数执行完后再执行,有点 js 中 setTimeout 的感觉。

func main() {
    defer func(){
        var p = recover()
        println("here xxxx", p)
    }()
    var i int = div(0)
    println(i)
}

这样一加,你就等于把异常处理了,能看到正常的 here xxxx 输出。

要手动抛出异常,可以使用 panic() 函数,它接受任何参数(反正是一个空接口):

func main() {
    defer func(){
        var p = recover()
        println("here xxxx", p)
    }()
    panic("hello")
    println("over")
}

看起来没异常的样子,倒像是一个 pipepanic 的调用值通过 recover() 传递出去了。

go 没有 try 的结构,异常只会延函数调用往上传播,直到被捕获:

package main


func aaa() {
    panic("throw in aaa")
}

func aa() {
    defer func(){
        var p = recover()
        if p != nil {
            println("aa", p)
            panic(p)
        }
    }()
    aaa()
}

func a() {
    defer func(){
        var p = recover()
        if p != nil {
            println("a", p)
        }
    }()
    aa()
}

func main() {
    a()
    println("over")
}

7. goroutine, 通道, 并发

这些被看成是 go 的特点。 goroutine ,有些人称其为 go程,或者“协程”,我喜欢叫它“独立上下文”。

通道 是一种数据类型,听说是用于不同独立上下文的信息交换,行为类似操作系统的 pipe

goroutine 肯定是很轻量的东西,随时创建,创建销毁。但是我目前不清楚,它们到底是不是可以“并行”, go 本身是不是可以在多个进程间调度它们,在保持看起来是同一个运行时的前提下。

go 语句可以开启一个 goroutine

func main() {
    go func(){
        println("in go")
    }()
    println("over")
}

直接运行,会发现看不到 in go ,打印 over 之后就结束了,嗯……,暂停一下才能看到 go func() 的输出:

package main

import (
    "time"
)

func main() {
    go func(){
        println("in go")
    }()
    println("over")
    time.Sleep(5 * time.Second)
}

go 原生提供了 goroutine ,但不像 js 那种会默认自己控制一个 event loop ,也不像 Python 的某些工具,会显式地提供 event.start()

不过 通道 倒是提供了默认的“阻塞”特性:

package main

func main() {
    var ch = make(chan string)
    var callback = func() {
        println("in go")
        var s string = <-ch
        println(s)
        println("here")
    }
    go callback()
    ch <- "write"
    println("over")
}

这段代码可以确定地得到:

in go
write
here
over

这样的输出,看起来就像是 ch <- "write" 之后,再执行 callback() 一样。

如果不把 ch 的读放在 callback() 中,在外面就立即读出:

go callback()
ch <- "write"
var s string = <-ch
println(s)
println("over")

那么编译时就会给出死锁错误。

关于死锁,我试过,先读或者先写,都会死,似乎只有通过 goroutine “同时”读写才行。

但是,如果从来就不往 ch 里写任何东西,又可以正常编译:

func main() {
    var ch = make(chan string)
    var callback = func() {
        println("in go")
        var s string = <-ch
        println(s)
        println("here")
    }
    go callback()
    println("over")
}

最终只会有 over 输出就是了。

挻矛盾的,至少 <-ch 会阻塞的说法不准确。

func main() {
    var ch = make(chan string)
    var callback = func() {
        println("in go")
        var s string = <-ch
        println(s)
        println("here")
    }
    go callback()
    time.Sleep(5 * time.Second)
    ch <- "write"
    println("over")
}

这段代码,最终只会输出:

in go
over

目前搞不懂。

下面来求证最开始的那个问题, goroutine 是否在多进程上调度,写个死循环:

package main

func main() {
    var count int = 0
    for {
        go func(){
            count += 1
            println(count)
            for {}
        }()
    }
    println("over")
}

通过操作系统的监控,可以发现能跑满所有 CPU 核心,哈,这个机制还是很可以的,特别是针对计算密集的场景。

不过剩下的问题是,go 中哪些数据类型是进程并行安全的?

8. 指针,空间分配和存续

go 里面有像 C 中一样的指针,同时可以使用 new() 来分配一块指定类型所需大小的空间:

var p *int = new(int)
*p = 123
println(*p)
println(p)
return

语法上,使用 * 对指针进行求值,使用 & 可以获取地址(并赋值给指针):

var m = map[string]string{"a": "123"}
var p *map[string]string
p = &m
fmt.Println((*p)["a"])

new 分配的空间,由 go 统一管理,即使退出函数,也不会直接释放:

func alloc() *int {
    var p *int = new(int)
    return p
}

func main() {
    var p *int = alloc()
    *p = 123
    println(p)
    println(&p)
    println(*p)
}

仅对于 切片映射通道 ,可以使用 make() 来分配空间(但是也可以不单独调用 make ,而是声明时直接完成初始化了,这时,空间的分配由系统自动处理,可能会比初始值大):

var s []int = []int{1,2,3}
var s2 []int
s2 = make([]int, 20)
fmt.Println(s)
fmt.Println(s2)
return

go 里没有单独的指向函数的指针,也不需要,函数本身就是“第一公民”。

9. 链表

链表的实现在 container/list 中的两个结构, ListElement

Element 比较简单,成员有:

List 的方法多一些:

简单的例子:

package main

import (
    "container/list"
    "fmt"
)


func show(l *list.List) {
    var e *list.Element
    e = l.Front()
    if e == nil { return }
    for {
        fmt.Print(e.Value)
        e = e.Next()
        if e == nil { break }
        fmt.Print(" -> ")
    }
}


func main() {
    var p *list.List = list.New()
    (*p).PushBack(2)
    p.PushBack("3")
    show(p)
}

用自己 PushBackList 的例子:

func main() {
    var p *list.List = list.New()
    var e *list.Element = (*p).PushBack(2)
    p.PushBack("3")
    show(p)
    println("\n\n")
    p.PushBackList(p)
    e.Value = 88
    show(p)
}

最后的输出是: 88 -> 3 -> 2 -> 3 ,能看出 p 是复制了一份添加的。

10. 字符与字节

10.1. rune, byte, string

先说一个限制,或者说一个前提,go 的源码被限制为必须使用 UTF-8 编码。

前面说过, rune 是“字符”, byte 类型是字节,而 “字符串” 是 string ,所以注意,这里其实有三种数据类型。

func main() {
    var s = "中文"
    fmt.Println(len(s))
}

上面的输出是 6 ,显然,“字符串”应该被叫作“字节串”。

这几个基本类型的转换在 go 中,倒是很直观,直接“声明”就可以完成转换了:

func main() {
    var s = "中文go"
    var ss = []rune(s)
    fmt.Println(len(ss))
}

转换成 []rune 就可以正确得到“字符个数”。 byte 同理:

func main() {
    var s = "中文go"
    var ss = []byte(s)
    fmt.Println(len(ss), len(s))
}

转成字符串也是直接的:

func main() {
    var b = []byte{'\x01', '\x64'}
    var ss = []rune{'中', '文'}
    fmt.Println(ss)
    var s = string(ss)
    var bs = string(b)
    fmt.Println(s, bs)
}

这些转换,都是基于 UTF-8 这个前提。对于其它编码的情况怎么处理呢?那只能依赖额外的模块了。

目前使用的是 golang.org/x/text

package main

import (
    "fmt"
    "golang.org/x/text/encoding/simplifiedchinese"
)

func UTF8toGBK(s []byte) []byte {
    b, err := simplifiedchinese.GBK.NewEncoder().Bytes(s)
    if err != nil {
        panic(err)
    }
    return b
}
func GBKtoUTF8(s []byte) []byte {
    b, err := simplifiedchinese.GBK.NewDecoder().Bytes(s)
    if err != nil {
        panic(err)
    }
    return b
}


func main() {
    var b []byte = UTF8toGBK([]byte("中文"))
    fmt.Println(b)
    var u []byte = GBKtoUTF8(b)
    fmt.Println(u)
    var s = string(u)
    fmt.Println(s)
}

基本上,go 中的做法都是基于 UTF-8 的字节数据做直接的转换操作。

相较而言, Python3.x 中的字符串是抽象的真正的“字符串”,和字节没有直接关系的处理方式,我觉得是更现代的做法。

10.2. strings 更多及“零值可用”

string 在 go 中,本来是一块不可变的字节内容,但是字符串的处理却又是很常见的场景,所以 go 提供了额外的工具来处理字符串的读写。

首先一定要记住的一点,就是“字节串”,而不是“字符串”:

package main

import (
    "fmt"
)

func main() {
    var s string = "中文"
    fmt.Println(s[:3])
    fmt.Println(string([]rune(s)[:1]))
}

上面的代码都会输出“中”字。

如果对字符串的读写处理,有更多的一些需求,比如性能(普通的字符串拼接因为要重新分配空间,所以频繁的操作成本还是比较大的), 那么 go 中有专门的工具:

package main

import (
    "strings"
)

func main() {
    var s strings.Builder
    println(s.String(), s.Len(), s.Cap())
    s.WriteByte('a')
    s.WriteString("哈哈")
    s.WriteRune('中')
    s.Write([]byte("文"))
    println(s.String(), s.Len(), s.Cap())
    s.Grow(100)
    s.WriteString("123455669990")
    println(s.String(), s.Len(), s.Cap())
    println(strings.ToTitle(s.String()))
    s.Reset()
    println(s.String(), s.Len(), s.Cap())
}

关于上面的代码:

从这里看 go 的种种,真的谈不上美感,也许是实用吧。

11. 文件IO

11.1. 文件读写

go 中的文件相关功能,由 os 模块提供支持,但同时,还有像 io/ioutil 之类的工具封装。

os 中,有对 Filefd,及各种具体的文件类型,文件权限的处理。

我去翻 os 的 API 时,找到了两个“打开文件”的方法,一个是 Open() 另一个 OpenFile() ,第一个只是为“读”打开,第二个有“读写”。这时我才知道, go 中,是不支持“参数默认值”机制的。如果一定要做,可以通过“可变参数”自己折腾。

不支持“参数默认值”是一种设计上的选择,无疑会带来很多的不便,但是我并不认为会带来多少“意图清晰”的收益。

当然,如果你在网上搜索一下为什么 go 不支持参数默认值,就会看到很多“屁话”,这种感觉,就像很多人无脑说 OSX 的设计是多么好一样,好到把 Enter 用成“改名”都是一种优越感(吐)。

直接用 os 读取完整的文件,还是有些麻烦的:

package main

import (
    "fmt"
    "os"
    "strings"
    "io"
)

func main() {
    file, err := os.Open("/home/zys/temp/demo.go")
    if err != nil {
        fmt.Println(err)
        panic(err)
    }

    var data strings.Builder
    for {
        var buff []byte = make([]byte, 10)
        _, err := file.Read(buff)
        if err == io.EOF { break }
        data.Write(buff)
    }
    fmt.Println(data.String())
}

每次只能读取具体大小的字节,通过判断 EOF 决定下一步行为。

直接使用 ioutil 会方便一些:

package main

import (
    "fmt"
    "os"
    "io/ioutil"
)

func main() {
    file, err := os.Open("/home/zys/temp/demo.go")
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
    content, err := ioutil.ReadAll(file)
    fmt.Println(string(content))

    content2, err := ioutil.ReadFile("/home/zys/temp/demo.go")
    fmt.Println(string(content2))
}

写文件也是类似的:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.OpenFile("/home/zys/temp/demo.txt", os.O_RDWR|os.O_CREATE, 0755)
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
    n, err := file.Write([]byte("890"))
    n2, err := file.WriteString("中文")
    println(n, n2)
}

11.2. 标准输入输出

os.Stdin, os.Stdout, os.Stderr 是三个 File Like 的对象。

标准输出和错误输出都很简单:

package main

import (
    "os"
)

func main() {
    os.Stdout.WriteString("123")
    os.Stderr.WriteString("123")
}

输入处理直接地可以是:

package main

import (
    "os"
    "fmt"
    "strings"
)

func main() {
    var s strings.Builder
    for {
        var buff []byte = make([]byte, 1)
        _, err := os.Stdin.Read(buff)
        if err != nil {
            break
        }
        if buff[0] == '\n' {
            fmt.Println(s.String())
            break
        }
        s.Write(buff)
    }
}

11.3. File Like

(暂时没找到现成的方案)

12. 命令行参数

完善一些的工具,就是 flag 这个模块。不过最直接的处理是使用 os.Args

package main

import (
    "os"
)

func main() {
    for idx, args := range os.Args {
        println(idx, args)
    }
}

编译后执行:

./demo --abc = 123 -b 1 "1 2 3"

能看到输出的内容是:

0 ./demo
1 --abc
2 =
3 123
4 -b
5 1
6 1 2 3

基本上就是以空格分割,但是额外处理了引号。

13. 日志

go 官方自带了一个 log 模块,有基本的配置能力。但是最重要的 Level 没有,这就有点尴尬。自己处理的话,只能不同的 Level 单独定义一个“实例”,然后通过配置再处理 output 。当然,第三方有一些功能更完整的模块。

package main

import (
    "os"
    "log"
)

func main() {
    InfoLog := log.New(os.Stdout, "INFO ", log.Ldate|log.Ltime|log.Lshortfile)
    ErrorLog := log.New(os.Stdout, "ERROR ", log.Ldate|log.Ltime|log.Lshortfile)
    InfoLog.Print("here")
    ErrorLog.Print("here")
}

INFO 部分是 Prefixlog.Ldate...Flags ,就提供了有限的几个配置。

14. 测试

go 有自带测试支持,主要在两个方面。一是 go 这个命令行工具,专门有一组 go test 功能,这套功能配合约定的“文件名”,“函数”等,可以直接一键运行项目中的测试用例。另一方面,官方提供了 testing 这个包,里面有测试的基本的功能实现,但是没有断言……

14.1. 功能测试

这里的几个规则包括:

下面的代码写在 demo_test.go 文件中。

package main

import "testing"

func add(a int, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    t.Run("first", func(t *testing.T){
        if add(1, 2) == 3 {
            t.Fail()
        }
    })

    t.Run("second", func(t *testing.T){
        if add(1, 2) == 3 {
        }
    })
}

func TestXX(t *testing.T) {
    t.Run("first", func(t *testing.T){
        if add(1, 2) == 3 {
        }
    })

    t.Run("second", func(t *testing.T){
        t.Run("another", func(t *testing.T){
            if add(1, 2) == 3 {
                t.Fail()
            }
        })
    })
}


func setup() {
    println("setup")
}

func teardown() {
    println("teardown")
}

func TestMain(m *testing.M) {
    setup()
    m.Run()
    teardown()
}

执行的时候:

go test demo_test.go

这样只能看到 Fail 的用例。要看全部的测试用例,可以:

go test demo_test.go -test.v

14.2. 性能测试

它的几个规则包括:

package main

import "testing"
import "strings"

func add(a string, b string) string {
    return a + b
}

func add2(a string, b string) string {
    var s strings.Builder
    s.WriteString(a)
    s.WriteString(b)
    return s.String()
}

func BenchmarkAdd(b *testing.B) {
    b.Run("add", func (b *testing.B) {
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            add("1", "2")
        }
    })
    b.Run("add2", func (b *testing.B) {
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            add2("1", "2")
        }
    })
}


func TestMain(t *testing.M) {
    t.Run()
}

和功能测试一样的,只是 *testing.B 提供了不同的功能。

执行的时候:

go test -bench=. demo_test.go -test.v

15. 并发与锁

作为一个把并发并行作为特性来设计的语言,却还有如此复杂多样的处理机制。同时在语法层面,对于是否“并行安全”也没有单独的设计,这样,当你使用某个数据结构,或者某个方法的时候,除非看文档,否则,你是不知道它是否是并行安全的。如此,我觉得,可能是会存在层层加锁的问题。

15.1. go 的调度能力

这个前面提到过。下面的死循环代码是可以跑满所有 CPU 核心的:

package main

func main() {
    var count int = 0
    for {
        go func(){
            count += 1
            println(count)
            for {}
        }()
    }
    println("over")
}

但是不用 goroutine 的编译后的 go 程序,就没有特别之处了:

package main

func main() {
    var count int64 = 0
    var a float64 = 3.1415926
    var m = map[float64]float64{}
    for {
        for {
            count += 1
            a = a * float64(count) / float64(count)
            m[float64(count)] = a + 0.0001
            if count % 10000 == 0 {
                println(count, a)
            }
        }
    }
    println("over")
}

可以看到,程序会在多个 CPU 核心之间调度。

15.2. 竞态

即使是最简单的数据结构,最简单的操作,也不要以为它们是并行安全的:

package main

import "fmt"
import "time"

func main() {
    var count int = 0
    for i := 0; i < 100; i++ {
        go func(){
            for j := 0; j < 100; j++ {
                count += 1
            }
        }()
    }
    time.Sleep(5 * time.Second)
    fmt.Println("count is", count)
}

多执行几次,一定会看到 count 最后的结果是会比 10000 小。 go 虽然可以在多 CPU 核心之间并行调度,也可以帮你把所有进程同步的事做好,但是,数据结构及操作行为的并行同步的工作,必须要自己处理。也许有的数据结构是并行安全的,那也需要你自己确认。

15.3. 原子操作

“原子操作”的意思,是 CPU 指令中本身提供了并行安全的计算指令,直接在相应的内存地址上使用这些指令,那么 CPU 会保证它们的操作是原子性的,不会遇到并行计算问题。然而,这些操作基本只支持整数,也只支持有限的一些简单运算。

go 通过 sync/atomic 包来提供这方面的支持。

package main

import "fmt"
import "sync/atomic"
import "time"

func main() {
    var count int32 = 0
    for i := 0; i < 100; i++ {
        go func(){
            for j := 0; j < 100; j++ {
                atomic.AddInt32(&count, 1)
            }
        }()
    }
    time.Sleep(2 * time.Second)
    fmt.Println("count is", count)
}

通过 atomic.AddInt32 执行的加 1,就不会有并行问题,所以,结果必然是 10000

atomic ,提供了针对整数和指针的这几类操作:

同时, atomic 还提供了一个比如通用的 Value 类型,我猜,可能是内部使用指针实现了一些操作吧。

LoadStore 单独看别以为是字面上那么简单,它后面还涉及“指令序”等知识。

所以,原子操作的功能,像我们这种对底层不太了解的人,尽量少碰了。

15.4. 互斥锁

互斥锁就是通常我们理解的“锁”,这里要注意,它是一种工具,而不是一种保证机制。工具的意思是,大家都用 Lock() ,那么可以达到同步的目的。而如果有些并行内容它本身不使用 Lock() ,那么它就可以无视其它并行使用了的 Lock()

go 中通过 sync.Mutex 提供锁的功能。

package main

import "fmt"
import "sync"
import "time"


func main() {
    var count int = 0
    var m sync.Mutex
    for i := 0; i < 100; i++ {
        go func(){
            for j := 0; j < 100; j++ {
                m.Lock()
                count += 1
                m.Unlock()
            }
        }()
    }
    time.Sleep(2 * time.Second)
    fmt.Println("count is", count)
}

同样是锁, go 中还有控制得更细一些的 RWMutex ,区分了读写的锁,它的规则是:

方法上子 Mutex 多两个:

前面的例子,加“写锁”可以,只加“读锁”是没用的:

package main

import "fmt"
import "sync"
import "time"


func main() {
    var count int = 0
    var m sync.RWMutex
    for i := 0; i < 100; i++ {
        go func(){
            for j := 0; j < 100; j++ {
                m.RLock()
                count += 1
                m.RUnlock()
            }
        }()
    }
    time.Sleep(2 * time.Second)
    fmt.Println("count is", count)
}

下面的例子,是模拟一个写要很长时间,而且中途还会写入不完整内容,读也要很长时间,而且还有多个读的场景:

package main

import "fmt"
import "sync"
import "time"
import "io"


func writer(callback func(string, error)) {
    for {
        fmt.Println("I am writing...")
        callback("xxx", nil)
        time.Sleep(5 * time.Second)
        callback(time.Now().Format("2006-01-02 15:04:05"), io.EOF)
    }
}


func main() {
    var current *string = new(string)
    var m sync.RWMutex
    go writer(func(s string, err error){
        if err == nil {
            m.Lock()
        }
        *current = s
        fmt.Println("Writed", s)
        if err == io.EOF {
            m.Unlock()
        }
    })

    for i := 0; i < 3; i++ {
        go func(){
            for {
                m.RLock()
                fmt.Println("I am reading...")
                time.Sleep(2 * time.Second)
                fmt.Println("Read out ", *current)
                m.RUnlock()
            }
        }()
    }

    time.Sleep(30 * time.Second)
}

不加锁,基本上永远只能读到不完整的 xxxRLock() 换成 Lock() 的话,读的部分就严重阻塞了,没有了并发能力。

15.5. 并发计数器

并发计数器,可以看成是一个简单的计数器实现,只是额外处理了并行安全。

go 中在 sync.WaitGroup ,主要有 3 个方法:

package main

import "fmt"
import "sync"

func main() {
    var counter sync.WaitGroup
    counter.Add(10)
    for i := 0; i < 10; i++ {
        var n = i
        go func(){
            defer counter.Done()
            fmt.Println(n)
        }()
    }
    counter.Wait()
}

使用还是比较简单的,不过要注意,不要在 goroutine 中调用 Add() ,可能在 Add() 之前 Wait() 都结束了,所以记得把 Add()Wait() 保持在同一个上下文中。

另外,这个工具和 goroutine 也没有必然联系,如果你想,可以在任何场景下使用。

15.6. 锁的条件应用

这里说的“条件应用”,指的是 sync.Cond 。它是基于锁的一套上层工具,提供了一种跨上下文的“通知机制”,并且这个“通知”过程是伴随锁状态交替的。

sync.Cond 主要提供三个方法:

一个例子:

package main

import "fmt"
import "sync"
import "time"


func main() {
    var cond = sync.NewCond(new(sync.Mutex))
    var counter sync.WaitGroup
    var c = 10
    counter.Add(c)
    for i := 0; i < c; i++ {
        var n = i
        go func(){
            cond.L.Lock()
            cond.Wait()
            defer counter.Done()
            fmt.Println("start", n)
            time.Sleep(10 * time.Second)
            fmt.Println("end", n)
            cond.L.Unlock()
        }()
    }

    go func(){
        for {
            time.Sleep(5 * time.Second)
            fmt.Println("out")
            cond.Signal()
            //cond.Broadcast()
        }
    }()
    counter.Wait()
}

15.7. 层级 Context

前面介绍了 WaitGroup ,它是一个简单,限制比较大的工具。

Context 则是一个使用“通道”实现的,比较灵活的工具。它功能也很简单,就是在合适的时候,手动发出“取消信号”。这个“合适的时候”,包括内置的计时器。

package main

import "context"
import "fmt"
import "time"

func main() {
    var root = context.Background()
    var current = 0
    context, cancel := context.WithCancel(root)
    for i := 0; i < 10; i++ {
        go func() {
            for {
                current += 1
                time.Sleep(1 * time.Second)

                fmt.Println(current)
                if current > 20 {
                    cancel()
                    fmt.Println("cancel")
                }
            }
        }()
    }
    <-context.Done()
    fmt.Println("complete")
}

context 本身是层级结构, Background() 可以得到一个“根”, WithCancel() 又继续得到一个“子”。当 cancel() 的时候,这个信息会往父级传递,然后 context.Done() 这个通道会关闭。

注意,上面的例子,只是 main() 执行完毕,并没有显式地去处理 goroutine 的结束问题。

要显式处理 goroutine ,我搜索到的方式都是把 context 传递给一个函数,函数内部再处理 Done() ,感觉挻别扭的。

除了 WithCancel()context 还提供了 WithDeadline()WithTimeout() ,它们两个其实是一样的,前者是给一个绝对时间,后者是给一个相对时间(绝对时间等于当前时间加相对时间嘛)。

package main

import "context"
import "fmt"
import "time"

func main() {
    var root = context.Background()
    var current = 0
    context, cancel := context.WithTimeout(root, time.Second * 2)
    for i := 0; i < 10; i++ {
        go func() {
            for {
                current += 1
                time.Sleep(5 * time.Second)

                fmt.Println(current)
                if current > 20 {
                    cancel()
                    fmt.Println("cancel")
                }
            }
        }()
    }
    <-context.Done()

    fmt.Println("here")
    for {
        time.Sleep(5 * time.Second)
        break
    }

    fmt.Println("complete")
}

上面的例子,能看到 2 秒之后, Done() 通道就有动作了,这里 go func() 里还在 Sleep 。同时也能看到,虽然代码已经跳到 here 那里了,但是上面的 goroutine 其实仍然不受影响地继续执行着的。

要处理好 go func() 的退出,可以传入 context ,并使用 select

package main

import "context"
import "fmt"
import "time"

func main() {
    var root = context.Background()
    var current = 0
    ctx, cancel := context.WithTimeout(root, time.Second * 2)
    for i := 0; i < 1; i++ {
        go func(ctx context.Context) {
            for {
                select {
                    case <-ctx.Done():
                        fmt.Println("select return")
                        return
                    default:
                        fmt.Println("select default")
                        current += 1
                        time.Sleep(5 * time.Second)

                        fmt.Println(current)
                        if current > 20 {
                            cancel()
                            fmt.Println("cancel")
                        }
                }
            }
        }(ctx)
    }
    <-ctx.Done()

    fmt.Println("here")
    for {
        time.Sleep(5 * time.Second)
        break
    }

    fmt.Println("complete")
}

虽然时间上没有办法在 2 秒时就马上中断 go func() ,但是 select 中的 return 是总会执行到的,对于有死循环的 goroutine 可以避免泄漏。

15.8. 缓存共享池

这里的关键点不是“池”,而是“缓存”,所以在使用时要记得,它可能随时消失。

go 通过 sync.Pool 结构体提供了一个简单直接的并行安全的“单对象内存块缓存”方案。

它通过“复用”来减轻内存变动的消耗,对于应用来说,只要得到一个可用的“对象”,本身也不必在乎这个对象是新的,还是二手的。

package main

import "fmt"

type Foo struct {
    name string
    value string
}

func main() {
    var f *Foo = &Foo{}
    f.name = "123"
    f.value = "333"
    fmt.Printf("%v %p\n", f, f)


    var f2 *Foo = &Foo{}
    f2.name = "321"
    f2.value = "889"
    fmt.Printf("%v %p\n", f2, f2)
}

(这里我们通过指针的方式使用结构体,而不是直接引用,是为了避免“值传递”时的内容复制行为)

上面这个原始的例子是要拿两次 Foo ,然后每次给它们赋值 namevalue 之后,打印出来。

通过 ff2 的地址值,可以看出来它们在两块内存上。

package main

import "fmt"
import "sync"

type Foo struct {
    name string
    value string
}

func main() {
    var cache sync.Pool
    cache = sync.Pool{
        New: func() interface{} {
            fmt.Println("New")
            return &Foo{}
        },
    }

    var f *Foo = cache.Get().(*Foo)
    f.name = "123"
    f.value = "333"
    fmt.Printf("%v %p\n", f, f)

    f.name = ""
    f.value = ""
    cache.Put(f)

    var f2 *Foo = cache.Get().(*Foo)
    f2.name = "321"
    f2.value = "889"
    fmt.Printf("%v %p\n", f2, f2)
}

这个改进后的使用 sync.Pool 的例子,能看到 ff2 是在同一块内存位置,我们在 Put() 前把 Foo 的属性手动重置掉的话,对于 f2 来说,是用的旧东西,还是全新的东西,不重要。

16. 项目模块,包,名字空间

16.1. 安装环境

最开始说过如何安装,在安装完之后,可以使用:

go env

来查看当前的环境,比如:

GO111MODULE=""
...
GOBIN=""
GOCACHE="/home/zys/.cache/go-build"
GOENV="/home/zys/.config/go/env"
...
GOOS="linux"
GOPATH="/home/zys/go"
...
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/opt/go"
...
GOTMPDIR=""
GOTOOLDIR="/opt/go/pkg/tool/linux_amd64"
...
GOVERSION="go1.17.2"
...
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build2853245378=/tmp/go-build -gno-record-gcc-switches"

GOPATH 是后面安装的包会放的地方。 GOROOT 是整个环境的根目录。

16.2. 项目模块

go 中的 mod ,我理解就是一个“项目”的意思。类似于 Node 中的 package.json , go 中也有 go.mod

创建一个名为 foo 的目录,然后进入目录,键入:

go mod init zys.me/foo

就可以把当前目录做成一个 go 模块,它会在当前目录创建一个 go.mod 的文件,内容是:

module zys.me/foo

go 1.17

这里的 module 后面就是模块名,也算是一个顶级的名字空间。

之后,通过 go get 安装的依赖包,也会写入到 go.mod 中,比如:

go get github.com/fatih/color

这是一个终端的颜色封装,安装之后,查看 go.mod 会看到:

module zys.me/foo

go 1.17

require (
	github.com/fatih/color v1.13.0 // indirect
	github.com/mattn/go-colorable v0.1.9 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)

依赖的内容就添加上了。记得结尾那些 indirect

我们再新添加一个 foo/main.go 文件:

package main

import "github.com/fatih/color"

func main() {
    color.Cyan("你好")
}

执行能看到蓝色的字。

执行一下:

go mod tidy

整理依赖。再查看 go.mod ,结果变成了:

module zys.me/foo

go 1.17

require github.com/fatih/color v1.13.0

require (
	github.com/mattn/go-colorable v0.1.9 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)

从现在的配置能看出,项目直接依赖了 color 。其它三个,估计是 color 它依赖的,所以对我们来说是“间接”依赖。

刚才下载的 color 包,文件会保存到 /home/zys/go/pkg/mod/github.com/fatih/[email protected]/

能看出,依赖的处理,也考虑了版本号。

16.3. 包 package

在学习的过程中,只用到一个 main 的包就可以了。但是正式的项目,就可能会有多个包存在。

一个模块,看成是一个项目的话,那么一个模块中,就可能有多个包。

包通过在代码文件开头的 package 结构写明。一个包,可以来自多个文件,但是一个包的文件只能放在同一个目录中,反之也限制了一个目录只能有一个包。

如果我们要新加一个 text.go 的文件,并且把它放到新的 text 的包中,那么之前的代码结构就要做一些调整,直接地:

foo/main.go
foo/text.go

这各目录结构肯定不行,会报找不到包的错误。

需要变成:

foo/main/main.go
foo/text/text.go

这样, main 目录下是 main 包, text 目录下是 text 包,就没有问题。

text.go 的内容是:

package text

import "github.com/fatih/color"

func Echo() {
    color.Cyan("haha")
}

main.go 的内容是:

package main

import "zys.me/foo/text"

func main() {
    text.Echo()
}

能看到使用自己的 text 包,也是一个绝对地址引用。

这个地址,其实是“模块名+目录”,跟包名没有关系,跟源代码文件名也没有关系。先通过目录定位到了包,之后 main() 中的那个 text 才是包名。

如果我们把代码结构改成:

.
├── a
│   └── 123
│       └── t.go
├── go.mod
└── main
    └── main.go

main.go 也需要改成:

package main

import "zys.me/foo/a/123"

func main() {
    text.Echo()
}

这里还要注意一点, Echo() 这个方法名,必须首字大写,否则这个方法在 main 包中找不到。(首字大写表示 public

多说一点,这里的 zys.me/foo/a/123 起的是包的名字空间的作用,也是按目录起包的定位作用。如果 123 目录还有子目录,那么 123123/sub 就是不同的定位,表示的也是不同的包。不过即使是不同的包,也可能具有相同的名字。

说到这里,可以看出,项目依赖的东西( go get 安装的那些),应该说依赖的是“模块”,不是“包”。一个“模块”中可能有多个“包”。

而在使用时,却只是以“包”为粒度,先 import ,然后再用。

17. 最后自己的一点看法

作为比较新的静态编译类型的语言, go 并没有带来新的理念,它也不纠结是否自己有完备的一些模型。感觉很多地方,都是向着实用的方向去设计的。所以社区中也有声音认为 go 是在开历史的倒车。同时,相较于经常被拿来两者比较的 Rust , go 的上手难度要小很多,可能也是因为这个原因,很多公司会选择用 go 做应用层的开发。我个人对此,是不太能理解的。

go 相较于 C ,那肯定方便不少。但是应用层,对比的是 jvm 那套啊。即使先不论 jvm 系的多了虚拟机和容器两层抽象,在服务管理方面有先天的优势。go 没有 try/catch ,没有 classOO ,没有泛型。是,没有这些不影响实现功能,但是要考虑开发人员的心智嘛。纯函数式的机制,我认为确实有比 classOO 更高的抽象能力,但是代价却是开发人员的额外设计投入,模型推演上花的额外的时间。对于应用层开发,这点上显然是得不偿失的。业务本身的变化又不会看技术设计的上限。所以简单直接,方法论完整的 classOO 就是一个最好的选择。

另外一点, go 提供了 goroutine 这个工具,如果用到网络编程的场景,它本质上跟“线程/进程”模型,是一样的。不会因为资源代价更小,就改变了的这个模型先天的问题。现在都 2021 年了,异步 IO 的模型应该已经成为大家的共识,现在又倒回“线程/进程”模型(go 的底层虽然用了 epoll ,但是官方包的上层仍然是使用 goroutine 做的同步模型)。有人做过测算,如果一个 goroutine 占 4KB 的内存,那么单机收到 100 万连接请求的话,就会占到 8G 的内存(IO的读写,go 中好像是分成 2 个 goroutine 处理的)。问题是,应用层收纳了 100 万的请求连接,除了占内存,并没有什么用啊。CPU 的核心只有 8 个或者 16 个,数据库的连接也是瓶颈。不要让连接过早进入应用服务,留在网络层调度要容易得多。当然,如果这 100 万都是需要长期保持连接的直连长连接,那 go 确实是非常合适的方案。除了官方库,其它的第三方网络相关的实现,也能找到异步 IO 的。

同时, go 提供了很容易创建的 goroutine ,但是“并行同步”都是传统那一套,加锁。“通道”不加锁,但它本身也是阻塞的。应用层开发几乎用不到了“并行编程”的,但是 go 的机制却很可能给很多人提供了一个犯错的机会——并行编程真的很难很难, go 没有把它变得简单。

所以,我认为 go 适合做计算密集偏向的那些网络服务,比如,各种底层基础设施的服务端。但是不适合应用层开发。

评论
©2010-2021 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax