传说中的go语言圣经,得看一看(•̀⌄•́)
—— By Jihan
主题:GO语言学习
类别:计算机->实用性+理论性论述书->golang
概要:从语法,特性,接口,工具,测试等方面系统性介绍go语言。总体来说是针对语言本身特性来进行介绍的。
前言
最近在设计一个项目的框架,所使用的就是golang语言,期望多看看书以及学习一些源码,从而有更优雅的架构设计。
书中有许多的练习程序,手动写代码,跑程序也是很重要的。
主要内容
太过于基础的语法,并不会在笔记中出现,这里主要记录一些go语言编写中需要注意的点。
基础语法
主要包含一些语法和关键语句特性相关内容。
-
如果传参使用
map
或者slice
实际传递的是引用的拷贝,也就是指针的拷贝,那么函数里对map的操作对调用者来说也是可见的。比如下面的代码:1
2
3func insertMap(m map[int]string){
m[1] = "1"
} -
switch
中的值会和每一个case
进行比较,直到找到匹配的值。因此不要在case
中放耗时的操作。select
也是同样如此。 -
短变量声明一般用作局部变量中。示例:
1
2
3in,err := os.Open(file)//声明两个变量in,err
out,err := os.Open(out)//声明变量out,赋值变量err
out,err := os.Open(out1)//报语法错误 -
多重赋值的时候,会先完全计算完右边的值,再赋值给左边。
1
x,y = x-y, x+y//可以完全得到预想的值,即x=x[旧]-y[旧], y=x[旧]+y[旧]
-
字符串
string
是不可变字节序列,不可越界,不可修改,运算只能产生新的变量,而非修改原有值。 -
无类型常量
const
往往拥有更高的精度和更广的取值范围,并且能够适用于很多地方。 -
数组的长度也是数组类型的一部分。
- 数组初始化:
1
2
3q := [...]int{1,2}//同r类型,...会自己根据初始化数据个数填入
r := [2]int{1,2}
c := [...]int{99:-1}//len(c)=100, 除去c[99] = -1,其他都是0 -
数组直接传递,是值传递,不同其他语言是引用传递。如果想使用引用传递,需要定义成指针类型:
func zero(a *[32]int)
-
slice
为空的判断,使用len(s) == 0
而不是s == nil
-
go语言有可变长度的栈,最大可以达到1G,能更加安全的使用递归
-
defer
函数的实际执行顺序以调用顺序的倒叙执行。- 当发生
panic
时,所有defer
也将以倒叙执行。
- 当发生
-
内置函数
recover
只能在defer
中调用,并且当发生panic
时能终止当前的宕机,并且从发生panic
的位置正常返回(不会往下执行)。- 通常情况下,不建议在
recover
中进行程序恢复,这样更容易掩盖程序中的bug - 常用
recover
的方式是提供一些更加有用的信息。
- 通常情况下,不建议在
函数及变量作用域
-
作用域:
- 整个程序级别:
int
,new
等内置类型或函数 - 包级别:同一个包的任何地方引用,比如包里的全局变量
- 文件级别:只有导入了该包才能使用的。比如
fmt.Print
- 块儿级别:函数内部,循环内部等。
- 整个程序级别:
-
函数作为参数的时候,传递的作用域问题,本质上函数参数传递的是指针,那么使用的时候也是直接使用指针指向的函数,只要指针本身有效(不是野指针),函数都能正常执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func runFunc() {
s := SendVar{
1,
2,
}
func_scope.CallBack(func(i int) error {
fmt.Println("inner call back func", s.inner, s.Out)
return nil
})
func_scope.CallBack(print)
func_scope.CallBack(s.print) //这里如果,s对象不存在了过后,容易导致函数指针读取错误。导致panic
}
func (s SendVar) print(i int) error {
fmt.Println("call back print func", s.inner, s.Out)
return nil
}
func print(i int) error {
fmt.Println("call back print func", i)
return nil
} -
同第三点所说,当结构体作为参数的时候,其他包是否有权限读取内部变量。类似json这种结构体中的内部变量就不会进行转换。实际上,直接读取肯定是无法读取的,写代码都编译不过。但可以通过反射获取,示例:
1
2
3
4func main(){
vs := var_scope.VS{Out: 1}
fmt.Println("Print var_scope VS", vs)
}var_scope包:
1
2
3
4type VS struct {
Out int
inner int
}那么这种情况会输出:
Print var_scope VS {1 0}
,意思是fmt.Println
可以读取到包内部变量。实际看源码我们发现,在fmt.Println
中实际使用reflect.Value.Int()
函数直接获取值(其他类型有对应转换函数),是能获取到内部变量的,如果使用reflect.Value.Interface()
来获取值,就会进行安全检查:1
2
3
4
5
6if safe && v.flag&flagRO != 0 {
// Do not allow access to unexported values via Interface,
// because they might be pointers that should not be
// writable or methods or function that should not be callable.
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}那么,我们看下面的示例:
var_scope包1
2
3
4
5
6
7
8
9
10
11
12
13
14func PrintOtherStruct(data interface{}) {
val := reflect.ValueOf(data)
typeOfTstObj := val.Type()
for i := 0; i < val.NumField(); i++ {
fieldType := val.Field(i)
//fieldType.Interface() 函数提示不能返回域外的值的panic,可以通过CanInterface函数来判断
//fieldType.Int() 直接转换成对应类型的值,不会检测。
fmt.Printf("object field %d key=%s value=%v type=%s \n",
i, typeOfTstObj.Field(i).Name, fieldType.Int(),
fieldType.Type())
}
fmt.Println("var scope print", data)
}1
2
3
4
5
6
7
8
9
10
11
12
13type SendVar struct {
Out int
inner int
}
func main() {
s := SendVar{
1,
2,
}
var_scope.PrintOtherStruct(s)
} -
匿名函数,func关键字后没有变量的函数,例如:
strings.Map(func(r rune) rune{return r + 1}, "HAL-9000")
。匿名函数有一些特性- 匿名函数能够获取到整个词法环境,里层能够使用外层的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) //1
fmt.Println(f()) //4
fmt.Println(f()) //9
f2 := squares()
fmt.Println(f2()) //1
fmt.Println(f2()) //4
fmt.Println(f2()) //9
}这里的匿名函数是能够使用外层的变量x的。并且变量x在调用
squares
返回后亦然存在(虽然是隐藏在变量f中,有点像var x int; func()int{...}
整个一体为f
变量一样)。
2. 匿名函数作为递归,必须先申明再赋值,直接声明赋值会导致未定义错误:1
2
3
4
5visitAll := func(items []string){
//...
visitAll(m[item]) //compile error: undefind visitAll
//...
}- 匿名函数在for循环中的外部变量引用,先看示例:
1
2
3
4
5
6
7
8
9var rmdirs []func()
dirs := tmpdirs()
for i:=0;i<len(dirs);i++{
//a := i //后面os.RemoveAll(dirs[i])替换为os.RemoveAll(dirs[a])才正确
os.MkdirAll(dirs[i],0755)
rmdirs = append(rmdirs, func(){
os.RemoveAll(dirs[i])//不正确
})
}在循环里(指
for
开始到{
中间)创建的变量,都是共享相同的变量,意味着匿名函数在使用的时候都是指向的一个地方。而如果内部声明的变量,如a:=i
里的a
则每次都会声明一个变量,隐藏在匿名函数中,而不会释放。 -
指针类型成员函数,使用非指针类型成员也能正常调用,golang会自动对指针和指针指向的值进行转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package main
import (
"fmt"
)
type A struct{
Num int
}
type B struct{
A
}
func (a *A)set(num int){
a.Num = num
}
func main() {
a := A{}
a.set(222)
fmt.Printf("A:%v\n",a)
b := B{}
b.set(11)
fmt.Printf("B:%v\n",b)
}输出:
1
2A:{222}
B:{{11}}
接口和反射
-
nil
也可以作为类方法的接收者,比如var a *NewStruct= (* NewStruct)(nil)
,但是通常不会这样使用。 -
接口就是提取一系列具体类型的共性而构成的。很多的常用系统函数都是提供接口值传递,支持自定义的类型传递(实现接口就行)。
-
接口值由两部分组成:类型和动态值。
- 如果动态值可以比较(不是map或者slice这类),那么接口值也是可以比较的。
var w *io.writer = os.Stdout
。接口值里的类型是*os.File
,对应值是&os.File{fd=1}
- 注意含有空指针的非空接口:
1
2
3
4
5
6
7
8
9
10func main(){
var a *os.File
check(a)
}
func check(w *io.writer){
//注意,这个地方传入的w,其接口值不为空。因为接口值存在非空类型值`*os.File`
if w == nil {
panic("w is nil")
}
} -
error
是一个接口,定义如下:1
2
3type error interface{
Error() string
} -
断言:
f, ok := x.(T)
-
有些函数包可以通过断言
error
的类型来判断错误类型。 -
可以通过断言来判断某个接口里是否包含某个方法。
1
2
3
4
5
6
7
8
9func writeString(w *io.Writer, s string)(int, error){
type stringWriter interface{
WriteString(string)(int, error)
}
if sw, ok := w.(WriteString); ok{
return sw.WriteString(s)
}
....
} -
反射
reflect
- 反射由两部分构成:Type和Value。对应着interface的类型值和类型值(实际数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73//一个反射的示例
package goreflectexample
import (
"fmt"
"reflect"
"strconv"
)
func formatAtom(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return "Invalid"
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
//....简化起见,省略浮点数和复数
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return v.String()
case reflect.Chan, reflect.Func, reflect.Ptr,
reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: //reflect.Array, reflect.Struct, reflect.Interface
return v.Type().String() + " value"
}
}
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = Invalid\n", path)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
display(fmt.Sprintf("%s[%d]\n", path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
feildPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(feildPath, v.Field(i))
}
case reflect.Map:
// 另外一个遍历map的方式
// iter := v.MapRange()
// for iter.Next() {
// k := iter.Key()
// va := iter.Value()
// display(fmt.Sprintf("%s[%s]\n", path, formatAtom(k)), va)
// }
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]\n", path, formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf("%s = Nil\n", path)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // 基本类型、通道、函数
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T)\n", name, x)
display(name, reflect.ValueOf(x))
}当然,上述函数还有优化点,对于多处字符串
+
的操作,使用bytes.Buffer
则更能提升性能。
2. 我们可以通过(Value).Set
函数来对可寻址的变量进行赋值。是否可寻址(获取变量指针)可以用(Value).CanAddr()
来确定。当然也可以通过取地址+断言的方式来获取指针,进行赋值:px := x.Addr().Interface().(*int); *px=3
3. 反射可以获取到未导出字段的值,但是不能通过反射来更新这些值。
4. 可以通过反射获取结构体字段标签
比如json中使用的json:"name"
可以通过reflect.StructField.Tag.Get("json")
来获取相应的值。
5. 可以通过反射获取相应方法:(Value).Method(i)
,函数数量可以通过(Value).NumMethod()
来获取。 -
使用反射的注意事项(总结:没有必要用反射,就不要用反射):
- 反射代码是很脆弱的,很容易出bug,并且bug是在遇到对应类型的时候以
panic
的方式抛出。 - 反射无法做静态类型检测,特别是
interface{}
或reflect.Value
参数时,一定要说明参数访问和其他限制。并且大量使用反射也不便于理解。 - 使用反射是比特定类型优化的函数低一两个数量级。有些关键性的核心函数,还是要避免使用反射。
- 反射代码是很脆弱的,很容易出bug,并且bug是在遇到对应类型的时候以
-
unsafe
包中包含了许多接近底层的一些操作,当然这些操作在C语言里面可能是常见的。如果不是很有必要,并不建议使用unsafe包,unsafe包会导致更不易察觉的错误,以及削弱代码可移植性。unsafe.Pointer
指向一个T
类型的指针,类似C里的void *
。unsafe.Pointer
可以转换为uintptr
,然后对指针进行数值计算。但是注意如果使用uintptr
作为中间变量,由于其类型为一个数据,那么其对应的值所指向的值可能被垃圾回收移动或者回收,导致不易发现的错误。如:
1
pT := uintptr(unsafe.Poniter(new(T))) //语句执行后,创建的T就要被回收,pT值就变成野指针。
如果要使用uintptr,需要谨慎使用,保证使用的最小范围。
-
reflect.DeepEqual
深度比较,会进行两个任意数据的类型,长度,和数据的比较。 -
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型
P
直接辨识的:- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接口接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接口接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
goroutine和通道
-
协程之间是没有父子关系的,所有子协程都是一个等级,挂在main函数下。
- 父协程退出,子协程不会退出
- 函数退出,函数内的协程不会退出(main函数除外)
-
管道类型
chan T
和map
类似,传递都是进行指针传递。1
2
3
4x, ok := <- ch
if !ok {
fmt.Fatal("channel closed.")
}- 管道可以手动关闭(也可以让gc自己回收),关闭后任何向管道里写入的操作都会崩溃,读取操作能够正常执行,读取完管道值后再读取的值就会零值。
- 单向通道:类型
chan<- T
只能发送的的通道,类型<-chan T
只能接收的通道。类型chan T
传值给单向通道的时候,都会进行隐式转换到相应的结构。 - 带有缓冲的通道,通道的数据存取类似于管道,先进先出。如果粗暴的在一个goroutine中将管道作为队列使用,可能会出现永久阻塞的风险。
- 在
select
中,如果通道是nil
,则改分支永远不会被选中,该特性可以用来启用或禁用某些特性。 - 可以在goroutine中使用管道作为令牌桶,控制goroutine的并发量:
1
2
3
4
5
6
7
8
9
10
11
12var tokens := make(chan struct{}, 10)
for i:=0;i<1000;i++{
go func(){
select {
case tokens <- struct{}: //获取令牌
case <-done: //用于控制进程取消操作。
return nil
}
defer func(){ <- tokens }() //释放令牌
//do something
}()
} -
不要通过共享内存来通信,应该使用通信来共享内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package bank
var deposits = make(chan int) //存钱
var balances = make(chan int) //查余额
func Deposit(mony int) { deposits <- mony }
func Balance() int { return <-balances}
func teller(){
var balance int //限制在一个goroutine内,不会读写冲突
for {
select {
case mony := <-deposits:
balance += mony
case balances <- balance:
}
}
}
fun init(){
go teller() //启动teller
} -
避免数据读写冲突的方法:
- 初始化后,变量只读
- 避免多个
goroutine
访问同一变量。比如30的例子,用管道通信的方式,避免了多goroutine
的同时读写。 - 使用互斥锁。
-
读写锁只有在竞争激烈时效果才比一般锁好,单个执行是不如一般锁,这是由于读写锁实现更复杂。
-
一些导致并行数据冲突的原因:
- 读写冲突导致
- cpu缓存会导致并发的时候读到错误的缓存值,而锁操作可以把缓存刷入内存。多核cpu会分配并发线程到不同cpu,而每个cpu都有自己的缓存,会读到过期的数据
- 并行程序在相互之间没有变量逻辑关联的时候,编译器可能会交换执行顺序。
-
延迟互斥初始化
sync.Once
- 一种延迟初始化的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20type memo struct{
mem map[string]string
mu sync.Mutex
ready chan struct{} //mem 准备好后关闭
}
func (m *memo)Get(key string) string {
m.mu.Lock()
if m.mem == nil{
//假设初始化的时候没有进行make
//第一次get数据时,进行初始化。
m.mem = make(map[string]string)
m.mu.Unlock()
close(m.ready)//广播数据准备完毕,并且只需要广播一次。
}else{
m.mu.Unlock()
//由于第一次广播数据时,关闭了消息,后续的访问,都不会阻塞。
<-m.ready
}
return m.mem[key]
} -
可以使用竞态检测器
race deleteor
来检测代码中是否有数据竞态的情况。使用方法在编译时增加-race
参数 -
goroutine和线程的差别
- goroutine有可增长的栈,而OS里的线程一般只有固定大小的2MB。goroutine栈的增长也是以2倍增长,通常初始值在2KB,最大一般可以到1GB
- 线程调度需要涉及到CPU上下文切换,而goroutine调度通常是由go调度器处理,而调度技术成为
m:n调度
(把m个goroutine调度到n个线程) - goroutine没有标识,而OS线程通常都有线程标识。
-
GOMAXPROCS
值表示:使用多少个OS线程来执行goroutine。正在休眠的goroutine不占用线程,阻塞IO、其他系统调用执行中或非go语言写的函数这些goroutine需要一个独立的OS线程。
测试
-
测试函数比如导入
testing
包,除去开头函数,可选后缀必须大写开头。 -
测试部分的代码,通常使用
_test.go
来进行区分,而包通常在一个里。 -
测试中
-run
命令后面跟的是正则表达式,运行匹配的函数。匹配字符里,可以忽略Test
头 -
测试中的
t.Fatal
函数必须heTest
在同一个goroutine
中。错误消息的一般格式"f(x)=y, want z" -
随机测试,构建符合模式的随机输入,来探测程序的边界。
-
测试程序和被测试程序,虽然都定义在一个包里,但是实际执行
go test
和执行主程序时,都是分开的。 -
测试程序中,不要调用
log.Fatal
或os.Exit
,这些调用会阻止跟踪的过程。通常认为这两个是main函数的特权调用。 -
白盒测试可以通过修改一些全局量来达到测试部分功能的目的,比如代码中使用了全局函数指针
var sendMsg = fun(string) error{...}
,我们可以在白盒测试中修改改指针,来避免发送出错。 -
外部测试包,通常用于外部调用来测试相应包,无法访问包内部变量(会单独存在一个包,package命名使用
<package>_test
)。其他则和内部测试相同。 -
有时候,外部测试包需要拥有对内部包的特殊访问权限,我们可以约定声明一个特殊的测试文件
export_test.go
,而其存放的内容,仅仅是把包内部功能暴露。如fmt
包中的export_test.go
:1
2
3package fmt
var IsSpace = isSpace
... -
如果我们要看我们自己编写的测试程序覆盖了多少源码,可以使用
go test -coverprofile=c.out ./
来查看。使用go tool cover
可以查看cover
的帮助。c.out会输出程序中哪些代码被覆盖,哪些没覆盖。 -
基准测试
Benchmark
,执行命令go test -bench=<name>
,同样<name>
使用正则匹配。如果不指定-bench
,则不执行基准测试。-benchmem
可以看到内存的操作消耗。详细测试可参考 -
我们可以通过
go test
做代码的性能剖析,不同的报告类别告诉我们不同的性能情况:1
2
3
4
5
6
7
8go test -cpuprofile=cpu.out //CPU占用
go test -blockprofile=block.out //阻塞操作
go test -memprofile=mem.out //内存占用
```
一个显示CPU性能的示例:
```go
go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http
go tool pprof -text -nodecount=10 ./http.test cpu.log其中
http.test
是执行go test
时产生的中间文件(如果go test
不带性能分析参数,那么就会删掉该中间文件)-nodecount=10
限制输出10行 -
Example
函数也会被go test
视为特殊函数。该函数必须通过语法编译,并且会关联Example
同名后缀的函数,并在文档中以示例的方式展示。也可以执行Example
函数,并且go test
会自动将执行结果,和你函数最后的带有// 输出:
后的注释进行比较,判断输出正确性。
环境及工具
- 工作空间
GOPATH
依赖的源码,现在通常都是通过go mod
来进行管理。环境变量GOROOT
,指定go的版本。go env
能配置这些变量。 - 进行
go build
时,后面的相对路径前必须加./
或../
之类的,否则就会识别为参数。 go build
和go install
非常相似,区别在于go install
不会丢弃编译代码和命令。它们对于没有修改过的包都不会进行重新编译。- 可以通过
go doc
来查看函数结构体等声明和注释,使用方法:go doc time
,go doc time.Since
等。当然我们自己也在包开发的时候也鼓励编写函数和包注释。 godoc
和go doc
功能一样,差别在于提供http的服务供网页查看。如godoc -http :8000
。-analysis=type
和-analysis=pointer
提供更丰富的文档。- 内部包是以
internal
为父目录的目录下的包,只能提供给internal
的父目录使用,其他地方无法使用。 go list
可以进行可用包的查询,...
为通配符。-json
参数可以让包已json格式输出,-f
可以通过text/template
模板来定义输出格式。- go的标准文件和
_test.go
文件在go test
时会分成两个包进行编译。你可以通过go list -json <package>
来查看包里的构成,GoFiles
包含的内容在go build
时用到,而TestGoFiles
则只会在go test
的时候用到。``XTestGoFiles`通常指外部测试列表 - 包的空导入
import _ image/png
,其作用是利用包中的init
函数进行相关组件的注册。同理,我们也可以通过init
函数来进行函数注册和模块注册。
约定及习惯
- 包名原则上和父目录的文件夹名称一致。如果不一致也能干活儿,但是你引用的
import git.com/pkg/A
的包,使用A路径下文件里函数的时候却是B.xxFunc
,不觉得这么干有病么。 - 如果类的方法使用的是指针接收者,那么所有的方法都建议使用指针接收者,如果方法使用结构体接收者,那么所有有方法建议都用结构体接收者。
- 接口命名通常是动词+er,比如
writer
,controller
等 - golang提倡平铺式结构,不提倡继承关系的结构。
- 程序结构,根据需求,以核心逻辑为主,细节由接口实现。
- 如果要提公共接口,公共接口往往专注一个能力,干一件事。不宜过长。
- 一个不错的接口设计经验:仅设计你所需要的接口
- golang命名建议:
- 变量、函数、结构体等建议驼峰命名
- 包名小写拼接,比如
strconv
,不建议str_conv
- 文件名看个人习惯,通常是小写拼接或者小写
_
拼接。基本除了驼峰类型,其他的都行。
- 好的测试程序:
- 测试程序也需要健壮性,和秩序跟进维护
- 测试程序要尽可能多的报告出错误信息,要尝试一次运行中报多个错误。
- 覆盖率
- 测试的约定函数名:
- Test开头,代表功能测试
- Benchmark开头,代表性能测试
- Example开头,代表示例函数
Cgo
-
cgo
用于在go中调用c的函数,当然如果你的c库很小,可以考虑移植它,如果性能对我们来说不是很关键,那么也可以用os/exec
来执行c的可执行程序。cgo
只用于复杂且对性能有所要求的C库。
一个C写的程序:1
2
3
4
5
6
7
8
9
10
11
12
13#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen){
s->next_in = in;
s->avail_in = *inlen;
s->next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s,action);
*inlen -= s->avail_in;
*outlen -= s->avail_out;
return r
}对应cgo部分的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package main
/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
*/
//标识加入cgo,程序会分析注释中的相关内容
import "C"
import (
"io"
"unsafe"
)
type writer struct {
w io.Writer
stream *C.bz_stream
outbuf [64 * 1024]byte
}
//NewWriter 对于bzip2压缩的流返回一个wirter
func NewWriter(out io.Writer) io.WriteCloser {
const (
blockSize = 9
verbosity = 0
workFactor = 30
)
w := &writer{w: out, stream: C.bz2alloc()}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
func (w *writer) Write(data []byte) (int, error) {
if w.stream == nil {
panic("closed")
}
var total int
for len(data) > 0 {
inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
C.bz2compress(w.stream, C.BZ_RUN,
(*C.char)(unsafe.Pointer)(&data[0]), &inlen,
(*C.char)(unsafe.Pointer)(&w.outbuf), &outlen)
total += int(inlen)
data = data[inlen:]
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return total, err
}
}
return total, nil
}
结语
作为golang语言的半入门者,看完这本书,算是入门了。总的来说,书还是很不错的,看完过后,可以让你对golang有个整体的概念,更加熟悉语言本身的特性,以及学习到一些使用技巧。值得入门级别的go语言学习者仔细阅读,其中的一些疑惑可以自己编写代码执行来深入了解。
注:这个笔记本身没有什么阅读学习价值,读书笔记对于我来说是在我学习的时候更加帮助我记忆和理解,以及后续遇到某个问题时的快速查阅。