Go Module
go env -w GOPROXY="https://goproxy.cn,direct"
get
- -u:下载并安装代码包,不论工作区中是否已存在它们
- -t:同时下载测试所需的代码包
编译
- 构建使用命令go build,安装使用命令go install 构建和安装代码包的时候都会执行编译、打包等操作,并且,这些操作生成的任何文件都会先被保存到某个临时的目录中
- 如果构建的是库源码文件,那么操作后产生的结果文件只会存在于临时目录中。这里的构建的主要意义在于检查和验证
- 库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用
- 如果构建的是命令源码文件(main),那么操作的结果文件会被搬运到源码文件所在的目录中
- 如果一个源码文件声明属于main包,并且包含一个无参数声明且无结果声明的main函数,那么它就是命令源码文件(对于一个独立的程序来说,命令源码文件永远只会也只能有一个。如果有与命令源码文件同包的源码文件,那么它们也应该声明属于main包)
- 如果安装的是库源码文件,那么结果文件会被搬运到它所在工作区的 pkg 目录下的某个子目录中
- 如果安装的是命令源码文件,那么结果文件会被搬运到它所在工作区的 bin 目录中
- 如果构建的是库源码文件,那么操作后产生的结果文件只会存在于临时目录中。这里的构建的主要意义在于检查和验证
- .a文件是编译过程中生成的,每个package都会生成对应的.a文件,Go在编译的时候先判断package的源码是否有改动,如果没有的话,就不再重新编译.a文件,这样可以加快速度
- 在运行go build命令的时候,默认不会编译目标代码包所依赖的那些代码包。当然,如果被依赖的代码包的归档文件不存在,或者源码文件有了变化,那它还是会被编译
- 如果要强制编译它们,可以在执行命令的时候加入标记-a。此时,不但目标代码包总是会被编译,它依赖的代码包也总会被编译,即使依赖的是标准库中的代码包也是如此
- 另外,如果不但要编译依赖的代码包,还要安装它们的归档文件,那么可以加入标记-i
- 那么我们怎么确定哪些代码包被编译了呢?有两种方法
- 运行go build命令时加入标记-x,这样可以看到go build命令具体都执行了哪些操作。也可以加入标记-n,这样可以只查看具体操作而不执行它们
- 运行go build命令时加入标记-v,这样可以看到go build命令编译的代码包的名称。它在与-a标记搭配使用时很有用
初始化
- init 的顺序由实际包调用顺序给出,所有引入的外部包的 init 均会被编译器安插在当前包的
main.init
之前执行 - 一个包内的 init 函数的调用顺序取决于声明的顺序,即从上而下依次调用
- 不管包被导入多少次,包内的init函数只会执行一次
格式化输出
/*
%v:默认格式输出
%f:浮点数输出
%s:字符串输出 以string格式打印,比如打印值是[]byte的时候 []byte{0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD}
%t:布尔值输出
%c:字符输出
%p:指针输出,十六进制方式显示
%b:整型以二进制方式显示
%o:整型以八进制方式显示
%d:整数以十进制方式显示
%x:整型以十六进制方式显示
整数字面量
0xF 十六进制表示(必须使用0x或者0X开头)
0XF
017 八进制表示(必须使用0、0o或者0O开头)
0o17
0O17
0b1111 二进制表示(必须使用0b或者0B开头)
0B1111
15 十进制表示(必须不能用0开头)
*/
func main() {
//p := Person{"hang", 12}
//fmt.Printf("%v", p) // {hang 12}
//fmt.Printf("%+v",p) // {Name:hang Age:12}
//fmt.Printf("%#v", p) // main.Person{name:"hang", age:27}
//fmt.Printf("%T", p) // main.Person
//a := []byte{0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD}
//fmt.Printf("%v ----- %s", a, a)
f := 3.1415926
fmt.Printf("%.2f", f)
}
for 和range
循环永动机
// 请问如下程序是否能正常结束?
1. func main() {
2. v := []int{1, 2, 3}
3. for i:= range v {
4. v = append(v, i)
5. }
6. }
能够正常结束。循环内改变切片的长度,不影响循环次数,循环次数在循环开始前就已经确定了(python不行,不支持遍历期间改变列表和字典的大小,会抛出异常),仅限于for-range语法,for不可以(for会无限添加)
对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha
,在赋值的过程中就发生了拷贝,而我们又通过 len
关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数
神奇的指针
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
// 3 3 3
正确的做法应该是使用 &arr[i]
替代 &v
这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2
变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,也就是指针指向每次循环中的同一个变量v
- range遍历字符串**时会把对应的字节并将字节转换成 **
rune
- range遍历slice,由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的
- range遍历slice,由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到
- hash表的遍历就是遍历所有的桶(起始桶的位置是随机的),所以新添加的元素可能出现在之前遍历过的桶中,这样就会有些新添加的元素可能遍历不到
- https://segmentfault.com/q/1010000012242735
- range遍历 channel是依次从channel中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。
- 作用于nil channel 会永久阻塞
函数
函数声明
type Printer func(contents string) (n int, err error)
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer
p = printToStd
p("something")
}
高阶函数
- 接受函数作为参数传入
- 把函数作为结果返回
type operate func(x, y int) int
func calculate(x, y int, op operate) (int, error){
if op == nil {
return 0, errors.New("invalid operate")
}
return op(x,y), nil
}
func main() {
// 此处使用匿名函数
op := func(x,y int) int {
return x + y
}
fmt.Println(calculate(1,2, op))
}
闭包
- 无论是for循环,还是range迭代,其定义的局部变量都会被重复使用(python 不会这样),这对闭包存在一定的影响
data := [3]string{"a", "b", "c"}
for i, s := range data {
println(&i, &s)
}
/*
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
*/
- 闭包是对外层变量的引用,又因为每次循环局部变量的地址不变,所以闭包函数最后执行时,所有闭包函数引用的都是同一个对象;解决办法,对局部变量重新赋值或者使用函数传递
- go 中用 for 遍历多次执行
goroutine
会存在什么问题? https://www.nowcoder.com/discuss/730415?source_id=discuss_experience_nctrack&channel=-1
func main() {
for i := 0; i < 3; i++ {
println(i, &i)
// 闭包传递
defer func(){ println(i, &i) } ()
}
}
func main() {
for i := 0; i < 3; i++ {
println(i, &i)
// 函数传递
defer func(i int){ println(i, &i) } (i)
}
}
func main() {
for i := 0; i < 3; i++ {
// 重新赋值
x := i
defer func(){ println(x, &x) } ()
}
}
可变参数
func sum(args ...int) int {
var result int
for _, v := range args {
result += v
}
return result
}
func Sum(args ...int) int {
// 利用 ... 来解序列
result := sum(args...)
return result
}
结构体
- 匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
- go 并没有实现继承,组合优于继承
type Animal struct {
name string
}
func (a *Animal) run() {
fmt.Printf("%v 会跑 \n", a.name)
//fmt.Printf(a.age) // 只能访问Animal中的属性, python继承可以
fmt.Printf("%#v", a) // &main.Animal{name:"阿奇"}
}
type Dog struct {
age int
*Animal //不是匿名的也可以,不是指针类型也可以(匿名的才更像继承)
}
func (d *Dog) fei() {
fmt.Printf("%v 会汪汪叫 \n", d.name)
fmt.Printf("%v", d.age)
}
func main() {
d1 := Dog{
age: 5,
Animal: &Animal{"阿奇"},
}
d1.run()
d1.fei()
}
结构体和nil
type Student struct {}
student := new(Student)
fmt.Printf("student 的数据类型为:%T,值为:%v\n", student, student)
fmt.Println("student == nill :", student == nil)
student 的数据类型为:*main.Student,值为:&{}
student == nill : false var student *Student 才是 true
可以看到,空结构体student并不是nil,而且其的值为 &{}
指针
- go 语言中对指针的限制:
- 指针不能参与运算
- 不同类型的指针不允许相互转换
- 不同类型的指针不能比较和相互赋值
- Go 语言在 unsafe 包里通过 unsafe.Pointer 提供了通用指针,通过这个通用指针以及 unsafe 包的其他几个功能可以让使用者绕过 Go 语言的类型系统直接操作内存,例如:指针类型转换,读写结构体私有成员这样的操作
- unsafe包只有两个类型,三个函数,但是功能很强大
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
// 内建类型,就像int
type uintptr uintptr
ArbitraryType
是int
的一个别名,在 Go 中ArbitraryType
有特殊的意义。代表一个任意Go
表达式类型。Pointer
是int
指针类型的一个别名,在 Go 中可以把任意指针类型转换成unsafe.Pointer
类型
三个函数的参数均是ArbitraryType
类型,就是接受任何类型的变量
Sizeof
接受任意类型的值(表达式),返回其占用的字节数Offsetof
:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)Alignof
返回变量对齐字节数量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct
类型,且还不能直接将这个struct
类型的变量当作参数,只能将这个struct
类型变量的值当作参数
以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行
unsafe.Pointer
unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:
- 任何类型的指针都可以被转化为
unsafe.Pointer
; unsafe.Pointer
可以被转化为任何类型的指针;uintptr
可以被转化为unsafe.Pointer
;unsafe.Pointer
可以被转化为uintptr
在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址,什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错
a := 1
b := unsafe.Pointer(a) //报错
b := unsafe.Pointer(&a) // 正确
unsafe.Pointer 指针支持和 nil
比较判断是否为空指针
unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型
// uintptr、unsafe.Pointer和普通指针之间的转换关系
uintptr <==> unsafe.Pointer <==> *T
uintptr
- uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64
unsafe.Pointer
指针可以被转化为uintptr
类型,然后保存到uintptr
类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是随便将一个uintptr
转为unsafe.Pointer
指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址
实操
指针类型转化
i := 10
var p *int = &i
var fp *float32 = (*float32)(unsafe.Pointer(p))
*fp = *fp * 10.12
fmt.Println(i) // 101
这里,我们将指向 int
类型的指针转化为了 unsafe.Pointer
类型,再转化为 *float32
类型,并进行运算,最后发现 i
的值发生了改变。
指针运算
// 可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值
type slice struct {
array unsafe.Pointer //元素指针
len int
cap int
}
s :make([]int,9,20)
var Len *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(8)))
fmt.Println(Len,len(s))// 9 9
var Cap *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(16)))
fmt.Println(Cap,cap(s))// 20 20
读写结构体的私有成员
- 通过 Offsetof 方法可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的
- 这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址
var x struct {
a int
b int
c []int
}
// unsafe.Offsetof 函数的参数必须是一个字段, 比如 x.b, 方法会返回 b 字段相对于 x 起始地址的偏移量, 包括可能的空洞。
// 指针运算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。
// 和 pb := &x.b 等价
pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性
如果改为下面这种用法是有风险的:
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
随着程序执行的进行,goroutine 会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个 unsafe.Pointer 是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr 类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp
,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp
也就不再是现在的 &x.b
的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃
string 和 []byte 零拷贝转换
string和[]byte 在运行时的类型表示为reflect.StringHeader
和reflect.SliceHeader
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type StringHeader struct {
Data uintptr
Len int
}
只需要共享底层 []byte 数组就可以实现零拷贝转换
func main() {
s := "Hello World"
b := string2bytes(s)
fmt.Println(b)
s = bytes2string(b)
fmt.Println(s)
}
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string {
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
错误处理与异常捕获
错误处理
自定义错误
- 我们经常会自己定义符合自己需要的错误类型,但是记住要让这些类型实现
error
接口,这样就不用向调用方暴露额外的类型 - 比如下面我们自己定义了
myError
这个类型,如果不实现error
接口的话,调用者的代码中就会被myError
这个类型侵入。即,下面的run
函数,在定义返回值类型时,直接定义成error即可
package myerror
type myError struct {
Code int
When time.Time
What string
}
func (e *myError) Error() string {
return fmt.Sprintf("at %v, %s",e.When, e.What)
}
func run() error {
return &MyError{
1002,
time.Now(),
"it didn't work",
}
}
- 那调用者判断自定义
error
是具体哪种错误的时候应该怎么办呢,myError
并未向包外暴露,答案是通过向包外暴露检查错误行为的方法来实现
myerror.IsXXXError(err)
- 抑或是通过比较
error
本身与包向外暴露的常量错误是否相等来判断,比如操作文件时常用来判断文件是否结束的io.EOF
if err != io.EOF {
return err
}
错误处理常犯的错误和解决方案
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err)
return err
}
return nil
}
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}
func main() {
err := WriteConfig(f, &conf)
fmt.Println(err) // io.EOF
}
上面程序的错误处理暴露了两个问题:
- 底层函数
WriteAll
在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层,因此在日志文件中得到一堆重复的内容 - 在程序的顶部,虽然得到了原始错误,但没有相关内容,换句话说没有把
WriteAll
、WriteConfig
记录到log里的那些信息包装到错误里,返回给上层
针对这两个问题的解决方案可以是,在底层函数WriteAll
、WriteConfig
中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误
一种简单的保证错误的方法是使用fmt.Errorf
函数,给错误添加信息
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("could not marshal config: %v", err)
}
if err := WriteAll(w, buf); err != nil {
return fmt.Errorf("could not write config: %v", err)
}
return nil
}
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write failed: %v", err)
}
return nil
}
fmt.Errorf
只是给错误添加了简单的注解信息,如果你想在添加信息的同时还加上错误的调用栈,可以借助github.com/pkg/errors
这个包,提供的包装错误的能力
//只附加新的信息
func WithMessage(err error, message string) error
//只附加调用堆栈信息
func WithStack(err error) error
//同时附加堆栈和信息
func Wrap(err error, message string) error
有包装方法,就有对应的解包方法,Cause方法会返回包装错误对应的最原始错误–即会递归地进行解包
func Cause(err error) error
下面是使用github.com/pkg/errors
改写后的错误处理程序
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace:\n%+v\n", err)
os.Exit(1)
}
}
总结
错误处理的原则就是:
错误只在逻辑的最外层处理一次,底层只返回错误
底层除了返回错误外,要对原始错误进行包装,增加错误信息、调用栈等这些利于排查的上下文信息
异常捕获
- recover函数的返回值是panic中传递的参数
- 如果没有使用 recover , panic 会一直向上抛
- 如果被调用函数自己处理了异常,不影响调用函数,只是自己终止,返回到调用函数
- 如果被调用函数自己没有处理异常,会一直向上抛,直到遇到一个处理异常的,相当于此处出现panic
func main() {
a()
}
func a() {
defer b()
panic("a panic")
}
func b() {
defer fb()
panic("b panic")
}
func fb() {
panic("fb panic")
}
panic: a panic
panic: b panic
panic: fb panic
最终程序先打印最早出现的panic,再打印其他的panic,嵌套panic不会陷入死循环,每个defer函数都只会被调用一次
将上面的程序稍微改进一下,让main函数捕获嵌套的panic
func main() {
defer catch("main")
a()
}
func a() {
defer b()
panic("a panic")
}
func b() {
defer fb()
panic("b panic")
}
func fb() {
panic("fb panic")
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
最终程序的输出结果为main recover:fb panic,这意味着recover函数最终捕获的是最近发生的panic,即便有多个panic函数,在最上层的函数也只需要一个recover函数就能让函数按照正常的流程执行
- panic 只会触发当前 Goroutine 的 defer;