记录一下Go语言的一些要点内容。
数据类型的默认值
数据类型 | 默认值 |
---|---|
int | 0 |
float64 | 0 |
string | "" |
bool | false |
pointer | nil |
slice | nil |
map | nil |
func | nil |
chan | nil |
interface | nil |
struct | 成员变量各自类型的默认值 |
按照底层结构划分,值类型包括(所有基本数据类型、数组、结构体),引用类型包括(slice、map、channel、function、interface、指针)。
Print:输出到控制台,不接受任何格式化操作
Println:输出到控制台并换行
Printf:打印出格式化的字符串,可以直接输出字符串类型的变量
Sprintf:格式化并返回一个字符串而不带任何输出
Fprintf:格式化并输出到 io.Writers 而不是 os.Stdout
fmt包
- 格式符:%v 占位符可以打印任何 Go 的值,%T 可以打印出变量的类型
var e interface{} = 2.7182
fmt.Printf("e = %v (%T)\n", e, e) // e = 2.7182 (float64)
- 打印指定宽度的数值
fmt.Printf("%10d\n", 353) // will print " 353"
还可以通过将宽度指定为 * 来将宽度当作 Printf 的参数,例如:
fmt.Printf("%*d\n", 10, 353) // will print " 353"
当打印出数字列表而且希望它们能够靠右对齐时,这非常的有用。 - 如果在一个格式化的字符串中多次引用同一个变量,可以使用 %[n],其中 n 是参数的索引位置(从 1 开始)。
- %v 占位符将会打印出 Go 的值,如果此占位符以 + 作为前缀,将会打印出结构体的字段名,如果以 # 作为前缀,那么它会打印出结构体的字段名和类型。
匿名函数和闭包
- 在多返回值的函数中,并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。
- 在Go语言中,所有的函数也是值类型,可以作为参数传递。
- Go语言支持常规的匿名函数和闭包。
- 匿名函数的执行方式是在函数体结束后以()调用。
- 闭包的本质不是一个包,而是一个函数,是一个持有外部环境变量的函数。
接口
- 接口和类型可以直接转换,甚至接口的定义都不用在类型定义之前。
方法
- 可以给内置类型(如int)增加新方法
如何选择方法的receiver类型
- 要修改实例状态,用*T
- 无须修改状态的小对象或固定值,建议用T
- 大对象建议用*T,以减少复制成本
- 引用类型、字符串、字典、函数等指针包装对象,直接用T
- 若包含Mutex等同步字段,用*T,避免因赋值造成锁操作无效
- 其他无法确定的情况都用*T
值传递和引用传递
传递类型 | 数据类型 |
---|---|
值传递 | 基本类型+复合类型(数组、结构体、指针) |
引用传递 | slice、map、channel、interface |
初始化顺序
- 全局变量(如果给全局变量赋值一个函数,则此函数先执行,优先于init函数)
- init函数
- main函数
Golang中实现协程间通讯有两种方式
- 共享内存型:使用全局变量+Mutex锁来实现数据共享。
- 消息传递型:使用channel机制进行异步通讯。
反射
反射概念
- Go没有像Java语言那样内置类型工厂,所以无法通过类型字符串创建对象实例。
- Go反射的两个基本概念:Type和Value。
- 对所有接口进行反射,都可以得到一个包含Type和Value的信息结构。Type代表类型信息,Value代表实例本身的信息。
- 获取类型信息:reflect.TypeOf(x)。
- Type和Value都包含了大量的方法,其中第一个有用的方法是kind,这个方法返回该类型的具体信息:Unit、Float64等。Value类型还包含了一系列类型方法,比如Int(),用于返回对应的值。
- 任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息。
package main
import (
"fmt"
"reflect"
_ "github.com/go-sql-driver/mysql"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age" test:"tt"`
}
func main() {
p := Person{
Name: "XiaoMing",
Age: 16,
}
typ := reflect.TypeOf(p)
for i := 0; i < typ.NumField(); i++ {
fmt.Println(typ.Field(i).Name, typ.Field(i).Tag)
}
fmt.Println(typ.Field(1).Tag.Get("test"))
val := reflect.ValueOf(p)
fmt.Println(val.Field(1))
}
---------------------
Name json:"name"
Age json:"age" test:"tt"
tt
16
反射输出
- 输出变量的类型:fmt.Println(reflect.TypeOf(b).Kind())
- Go并不能像Java那样通过类型字符串创建对象实例。
- reflect.ValueOf(Data)
- reflect.ValueOf(Data).Elem()
- reflect.TypeOf(Data).Elem().NumField()
- reflect.ValueOf(Data).Elem().NumField()
- reflect.ValueOf(Data).Elem().Type()
- reflect.ValueOf(Data).Elem().Type().Name()
... 可变参数/将切片打散
- 可变参数
(1)这个 ...T 类型等价于 []T 类型。
(2)当 ...string 形参实际传入的实参为nil时,其本质类型也是[]string。
- 将切片打散
src := []int{1, 2, 3}
dst := []int{4, 5}
dst = append(src, dst...)
fmt.Println(dst)
输出:
[1 2 3 4 5]
nil
- nil标志符用于表示interface、function、maps、slices和channels的“零值”。
- string的零值是"",而不是nil。
make
- make用于slice,map,和channel的初始化。
len和cap
- 在slice上可以使用len和cap。
- 可以在创建map时指定它的容量,但无法在map上使用cap()函数。
- 在channel上可以使用len和cap。
map
- 只要是任何定义了equal操作的类型都可以当做map的key,比如integers, floating point and complex numbers,strings,pointers,interfaces,channel,structs 和 arrays。
- func、slice、map不能作为key,因为它们没有定义equal操作。
- 对于struct、interface和array来说,如果它们要作为key,必须它们包含的元素都可以作为key才行。
- slice的元素是可以取址的,但map的元素是不可取址的,通过interface引用的变量也是不可取址的。
- 对于一个值为struct类型的map,那么无法更新其单个的struct值,因为map元素是无法取址的。有两种方法可以解决这个问题,一是使用临时的struct进行赋值,二是使用*struct作为值类型的map。
- map类型的取值操作总有值返回,Go会返回元素对应数据类型的零值,比如nil、'' 、false 和 0。
单行与多行
- 在单行的Slice、Array和Map,如果没加末尾的逗号,将不会得到编译错误。
- 在多行的Slice、Array和Map语句中如果遗漏最后的逗号,会提示编译错误。
不定参数
- 从底层实现的机制上来说,不定参数本质上是将传入的参数转化成数组的切片。
- 既然传入的是一个数组的切片,那为什么要专门设置不定参数,而不是直接规定传入一个切片呢?其实这个关键字简化的并不是函数的设计方,而是函数的使用方,这样使用方就不必强制转换成切片了。
- 如果不定参数传入interface{},这样就能使用不同类型的参数。可以用.(type)获取一个interface变量实际的类型,这样就实现了任意类型任意数量参数的传入。
log
- Go中原生的log.Fatal和log.Panic不仅仅是Log,当调用这些函数时,Go也将会终止应用。
并发安全
- Go本身有很多特性来支持并发,但并不保证其所有数据类型都是并发安全的,确保数据集合以原子的方式更新是开发者的职责。
- 由于一个进程内创建的所有goroutine运行在同一个内存地址空间中,因此如果不同的goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写锁。
- Go语言标准库中的sync包提供了完备的读写锁功能。
引用类型
- Go 的引用类型包括 slice、map、channel、function、pointer 等,它们在进行赋值时拷贝的是指针值,但拷贝后指针指向的地址是相同的。
- slice的源码在:src/runtime/slice.go,扩容处理在 growslice 函数中。
- map的源码在:src/runtime/map.go,结构体主要是hmap。
- channel的源码在:src/runtime/chan.go,结构体主要是hchan。
堆栈
- Go中变量的位置是放在堆上还是栈上是由编译器决定的。如果想知道变量分配的位置,在"go build"或"go run"上传入"-m" gc标志(即go run -gcflags -m main.go)。
同步原语
goroutine和channel的同步原语,在库层面有:
- sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和 工具类(Once、WaitGroup、Cond、Pool、Map)
- sync/atomic:提供原子操作(基于硬件指令compare-and-swap)
defer
- defer语句的含义是不管程序是否出现异常,均在函数退出时自动执行相关代码。
- defer执行的3个时机:
(1)含有defer的函数返回时
(2)含有defer的函数执行到末尾时
(3)defer所在的goroutine发生panic时
都会执行defer处理。 - 但当调用os.Exit()方法退出程序时,defer并不会被执行。
- defer在匿名返回值和命名返回值函数中的不同表现
package main
import "fmt"
func main() {
fmt.Println(returnValues())
fmt.Println(namedReturnValues())
}
func returnValues() int {
var result int
defer func() {
result++
fmt.Println("returnValues defer")
}()
return result
}
func namedReturnValues() (result int) {
defer func() {
result++
fmt.Println("namedReturnValues defer")
}()
return result
}
输出结果为:
returnValues defer
0
namedReturnValues defer
1
首先需要了解defer的执行逻辑,文档中说defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。
(1)将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
(2)然后检查是否有defer,如果有则执行
(3)返回刚才创建的返回值(retValue)
在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。
5. 当发生panic时,所在goroutine的所有defer会被执行,但是当调用 os.Exit() 方法退出程序时,defer并不会被执行。
Recover & Panic
- recover要与defer联合使用,并且不跨协程,才能真正的拦截panic事件;
- 每执行一次panic语句,就会创建一个_panic结构体基础单元;
- defer的基础单元是_defer结构体;
- 通过查看_panic和link字段可以得知,defer同时挂载着panic信息;
- 从代码实现来看,panic会触发延迟调用(defer),当defer中存在recover时,才会执行recover。也就是说,在panic时,Go只会对在defer中的recover进行检测;
- 在Go语言中,有一些panic的情况是无法recover的,即recover并非是万能的。比如panic的fatalthrow方法、fatalpanic方法等,他们一般在并发写入map等处理时抛出,需要谨慎。recover只对用户态下的panic关键字有效。
- panic只能触发当前goroutine的defer调用;
让Go Panic的十种方法
- 数组/切片索引越界
- 空指针调用
- 过早关闭HTTP响应体
- 除以零
- 向已关闭的通道发送消息
- 重复关闭通道
- 关闭未初始化的通道
- 未初始化map
- 跨协程的panic处理
- sync计数为负值
Go在容器运行时要注意的细节
- 在容器化的环境中,Go程序所获取的CPU核数是错误的,它所获取的是宿主机的CPU核数。
- 即使容器和宿主机的CPU核数是共享的,但在集群中一般会针对每个Pod分配指定的核数,因此实际上我们需要的是Pod的核数,而不是宿主机的CPU核数;
- 如果获取核数错误,可能会导致Go程序的延迟加大,程序响应缓慢;
- 解决方法:
(1)结合部署情况,主动设置正确的GOMAXPROCS核数
(2)通过cgroup信息,读取容器内的正确GOMAXPROCS核数
可以使用Uber公司推出的uber-go/automaxprocs开源库,它会在Go程序运行时根据cgroup的挂载信息来修改GOMAXPROCS核数,并基于一定规则选择一个最合适的数值。
Cgo
- Cgo的“hello world”示例
package main
// #include <stdio.h>
// #include <stdlib.h>
/*
void print(char *str) {
printf("%s\n", str);
}
*/
import "C"
import "unsafe"
func main() {
s := "Hello Cgo"
cs := C.CString(s)
C.print(cs)
C.free(unsafe.Pointer(cs))
}
工程管理
- Go命令行工具彻底消除了工程文件的概念,完全用目录结构和包名来推导工程结构和构建顺序。
问题追踪和调试
- 最常规的问题跟踪方法:打印日志、使用GDB进行逐步调试。
- Go语言编译的二进制程序直接支持GDB调试。Go编译器生成的调试信息格式为DWARFv3,只要GDB版本高于7.1都支持。
Json
- Go语言的大部分数据类型都可以转化为有效的Json文本,但channel、complex和函数这几种类型除外。
sync.Once
sync.Once可用于任何符合“exactly once”语义的场景,比如:
- 初始化 rpc/http client
- open/close 文件
- close channel
- 线程池初始化
go version
- 查看Go二进制文件的版本信息
go version [Go二进制文件的绝对路径]
- 查看Go二进制文件的go mod信息
go version -m [Go二进制文件的绝对路径]
多个init的调用顺序
- 不同的package,如果存在相互依赖,则最先调用最早被依赖的package中的init();
- 不同的package,如果不存在相互依赖,则按照main包中"先import的后调用"的顺序调用其包中的init();
- 同一个package中,不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数;
- 同一个package中,对同一个go文件的多个init()调用顺序是从上到下的;
逃逸分析
- Go语言的堆栈分配可以通过compiler去分析,通过GC去管理;
- 逃逸分析是一种确定指针动态范围的方法,即分析在程序中的哪些地方可以访问到该指针,从而确定一个变量是放在堆上还是栈上;
- 如果在其它地方(非局部)被引用,那么此变量一定是被分配到堆上;
- 即使没有被引用,如果对象过大,则依然有可能被分配到堆上;
- 注意:Go语言是在编译阶段确立逃逸的,并不是在运行时;
- 是否被作用域之外引用是逃逸的重要原因之一;
- 如果是未确定类型,比如使用了 fmt.Println(str) 进行打印,因为 func Println(a ...interface{}) (n int, err error) 的形参是interface{},这种在编译阶段无法确定具体类型,因此会造成逃逸,最终str变量会分配到堆上;
- 如果是泄露参数,比如一个指针参数传给函数之后,没有做任何引用之类的设计变量的动作,而是被直接原样返回,那这个变量实际上并没有逃逸,它仍然是被分配在栈上;
- 静态分配到栈上,一般会比动态分配到堆上性能要好;
- 底层分配到堆上还是栈上,一般来说对用户是透明的,无需过度关心;
- 每个Go版本的逃逸分析都可能有所不同,因为会不断优化;
- 处处使用指针传递不一定是最好的,建议合理使用;
竞态检测
竞争检测器已经完全集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。
$ go test -race mypkg // 测试包
$ go run -race mysrc.go // 编译和运行程序
$ go build -race mycmd // 构建程序
$ go install -race mypkg // 安装程序
goroutine的栈空间
- 从栈空间上,goroutine的栈空间更加动态灵活。
- 每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小对于goroutine来说,可能是一种巨大的浪费。
- 作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB。在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB),而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。