-
Notifications
You must be signed in to change notification settings - Fork 3
go learning notes
《Go 语言学习笔记》 第五版,作者:雨痕
未命名类型:数组、切片、字典、通道等与具体元素类型或长度等属性有关的类型
具有相同声明的未命名类型被视作同一类型
func main(){
data := [3]int{10, 20, 30}
for i, x := range data { // 会复制 data,从 data 复制品中取数据
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}
fmt.Printf("x: %d, data: %d\n", x, data[i])
}
for i, x := range data[:] { // 仅复制 slice,复制成本较低
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}
fmt.Printf("x: %d, data: %d\n", x, data[i])
}
}
输出:
x: 10, data: 110
x: 20, data: 220
x: 30, data: 330
x: 110, data: 210
x: 420, data: 420
x: 630, data: 630
如果 range 目标表达式是函数调用,也仅被执行一次。
变参本质上就是一个切片。只能接受一到多个同类型参数,且必须放在列表尾部。
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。
func test(a ...int) {
fmt.Println(a)
}
func main() {
a := [3]int{10, 20, 30}
test(a[:]...) // 转换为 slice 后展开
}
字符串是不可变字节(byte)序列,其本身是一个复合结构。
type stringStruct struct {
str unsafe.Pointer
len int
}
头部指针指向字节数组,但没有 NULL 结尾。默认以 UTF-8 编码存储 Unicode 字符,字面量里 允许使用十六进制、八进制和 UTF 编码格式。
func main() {
s := "编程\x61\142\u0041"
fmt.Printf("%s\n", s)
fmt.Printf("% x, len: %d\n", s, len(s))
}
// 编程abA
// e7 bc 96 e7 a8 8b 61 62 41, len: 9
cap 不接受字符串类型参数
func main() {
s := "编程"
for i := 0; i < len(s); i++ { // byte 1字节(uint8)
fmt.Printf("%d: [%c]\n", i, s[i])
}
for i, c := range s { // rune 4字节(int32),返回 Unicode 字符
fmt.Printf("%d: [%c]\n", i, c)
}
}
// 0: [ç]
// 1: [¼]
// 2: [�]
// 3: [ç]
// 4: [¨]
// 5: [�]
// 0: [编]
// 3: [程]
要修改字符串,须将其转换为可变类型([]rune 或 []byte),待完成后再转换回来。 但不管如何转换,都须重新分配内存,并复制数据。
func printDataPointer(format string, ptr interface{}) {
p := reflect.ValueOf(ptr).Pointer()
h := (*uintptr)(unsafe.Pointer(p))
fmt.Printf(format, *h)
}
func main() {
s := "hello, world!"
pp("s: %x\n", &s)
bs := []byte(s)
s2 := string(bs)
printDataPointer("string to []byte, bs: %x\n", &bs)
printDataPointer("[]byte to string, s2: %x\n", &s2)
rs := []rune(s)
s3 := string(rs)
printDataPointer("string to []rune, rs: %x\n", &rs)
printDataPointer("[]rune to string, s3: %x\n", &s3)
}
// s: 10b4329
// string to []byte, bs: c42000e290
// []byte to string, s2: c42000e2b0
// string to []rune, rs: c420018200
// []rune to string, s3: c42000e2e0
使用”非安全”方法进行改善。因为[]byte 和 string 头结构部分相同。
func toString(bs []byte) string {
return *(*string)(unsafe.Pointer(&bs))
}
func main() {
bs := []byte("hello, world!")
s := toString(bs)
printDataPointer("bs: %x\n", &bs)
printDataPointer("s: %x\n", &s)
}
// bs: c42000e260
// s: c42000e260
在以下场合编译器会进行专门优化,避免额外分配和复制操作:
- 将 []byte 转换为 string key,去 map[string] 查询的时候
- 将 string 转换为 []byte,进行 for range 迭代时,直接取字节赋值给局部变量
动态构建字符串也容易造成性能问题:
用加法操作符拼接字符串时,每次都须重新分配内存。
var s string
for i := 0; i < 1000; i++ {
s += "a"
}
// 可用 strings.Join 来改善,它会统计所欲参数长度,一次性完成内存分配
s := make([]string, 1000)
for i := 0; i < 1000; i++ {
s[i] = "a"
}
s1 := strings.Join(s, "")
// bytes.Buffer 也能完成类似操作,且性能相当
var b bytes.Buffer
b.Grow(1000) // 事先准备足够的内存,避免中途扩张
for i := 0; i < 1000; i++ {
b.WriteString("a")
}
s:=b.Stirng()
对于数量较少的字符串格式化拼接,可使用 fmt.Sprintf 、text/template 等方法
字符串操作通常在堆上分配内存,会有大量字符串对象要做垃圾回收。建议使用 []byte 缓存池,或在栈上自行拼装等方式 来实现 zero-garbage。
strings.Join 的实现:
func Join(a []string, sep string) string {
switch len(a) {
case 0:
return ""
case 1:
return a[0]
case 2:
// Special case for common small values.
// Remove if golang.org/issue/6714 is fixed
return a[0] + sep + a[1]
case 3:
// Special case for common small values.
// Remove if golang.org/issue/6714 is fixed
return a[0] + sep + a[1] + sep + a[2]
}
// 统计分隔符长度
n := len(sep) * (len(a) - 1)
// 统计所有待拼接字符串长度
for i := 0; i < len(a); i++ {
n += len(a[i])
}
// 一次性分配所需长度的数组空间
b := make([]byte, n)
// 拷贝数据
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
return string(b)
}
utf8 可用 utf8.RuneCountInString(s) 代替 len 返回准确的 Unicode 字符数量
GBK编码,一个汉字占两个字节。
UTF-16编码,通常汉字占两个字节,CJKV扩展B区、扩展C区、扩展D区中的汉字占四个字节(一般字符的Unicode范围是U+0000至U+FFFF,而这些扩展部分的范围大于U+20000,因而要用两个UTF-16)。
UTF-8编码是变长编码,通常汉字占三个字节,扩展B区以后的汉字占四个字节。(如"𦅰"为4个字节)
长度是数组类型组成部分。(元素类型相同,但长度不同的数组不属于同一类型)
对字典进行迭代,每次返回的键值次序都不相同。
字典被设计成“not addressable”,不能直接修改 value 成员(结构或数组)
正确做法是返回整个 value,待修改后再设置字典键值,或直接用指针类型
type user struct {
name string
age byte
}
func main() {
// 使用指针类型
m := map[int]*user {
1: &user{"Jack", 20},
}
m[1].age++
}
m[key]++ 是合法操作。不能对 nil 字典进行写操作,但却能读。
迭代操作不会取得新增的键值。(迭代次数为一开始的次数)
func main() {
m := map[int]int{
1: 3,
2: 3,
4: 5,
3: 9,
}
for i:=range m{
delete(m,i)
m[i+1]=20
m[i+2]=30
fmt.Println(i,m)
}
fmt.Println(m)
}
/*
1 map[2:20 4:5 3:30]
2 map[4:30 3:20]
4 map[3:20 5:20 6:30]
3 map[5:30 6:30 4:20]
map[5:30 6:30 4:20]
*/
在创建时预先准备足够空间有助于提升性能,减少扩张时的内存分配和重新哈希操作。
字典不会收缩内存。
空结构(struct{})是指没有字段的机构类型,无论是其自身,还是作为数组元素类型,其长度都为零。
这类”长度“为零的对象通常都指向 runtime.zerobase 变量
空结构可作为通道元素类型,用于事件通知。
所谓匿名字段(anonymous field),是指没有名字,仅有类型的字段,也被称为嵌入字段或嵌入类型
如嵌入其他包中的类型,则隐式字段名字不包括包名
因未命名类型没有名字标识,无法作为匿名字段
不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同
在分配内存时,字段须做对齐处理,通常以所有字段中最长的基础类型宽度为标准
func main() {
v3 := struct {
a byte
b []int //基础类型 int,对齐宽度 8
c byte
}{}
fmt.Printf("v3: %d, %d\n", unsafe.Alignof(v3), unsafe.Sizeof(v3))
}
// v3: 8, 40
// byte 8*1 + []int 8*3 + byte 1*8 = 40
如果空结构类型字段是最后一个字段,那么编译器将其当作长度为 1 的类型做对齐处理,以便其地址不会越界, 避免引发垃圾回收错误。
如方法内部并不引用实例,可省略参数名,仅保留类型。
接口代表一种契约,是多个方法声明的集合。
接口最常见的使用场景,是对包外提供访问,或预留扩展空间。
我们可以先实现类型,而后再抽象出所需接口。
###执行机制
内部实现:
type iface struct {
tab *itab
data unsafe.Pointer
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
通常以 er 作为名称后缀。方法名是声明组成部分,但参数名可不同或省略
将对象赋值给接口变量时,会复制该对象。
只有当接口变量内部的两个指针(itab,data)都为 nil 时,接口才等于 nil。
- 并发:逻辑上具备同时处理多个任务的能力。
- 并行:物理上在同一时刻执行多个并发任务。
多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。尽管协程再单个线程上通过主动切换来实现多任务, 但也有自己的优势。除了将因阻塞而浪费的时间找回来外,还免去了线程切换开销,有着不错的执行效率。 协程上运行的多个任务本质上是依旧串行的,加上可控自主调度,所以并不需要做同步处理。
用多进程来实现分布式和负载均衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的处理器资源, 用协程来提高处理器时间片利用率。
简单将 goroutine 归纳为协程并不合适,运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。 这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等, 可用 runtime.GOMAXPROCS 函数修改
Gosched 暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行。
Goexit 立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发 panic,也无法捕获。
如果在 main.main 里调用 Goexit,它会等待其他任务结束,然后让进程直接崩溃。
无论身处哪一层,Goexit 都能立即终止整个调用堆栈,这与 return 仅退出当前函数不同。 标准库寒素 os.Exit 可终止进程,但不会执行延迟调用。
同步模式必须有配对操作的 goroutine 出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。