Skip to content
cym edited this page Jan 15, 2018 · 1 revision

《Go 语言学习笔记》 笔记

《Go 语言学习笔记》 第五版,作者:雨痕

第 2 章 类型

未命名类型:数组、切片、字典、通道等与具体元素类型或长度等属性有关的类型

具有相同声明的未命名类型被视作同一类型

第 4 章 函数

range 复制目标数据

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 后展开
}

第 5 章 数据

字符串(string)

字符串是不可变字节(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)
}

Unicode

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 的类型做对齐处理,以便其地址不会越界, 避免引发垃圾回收错误。

第 6 章 方法

如方法内部并不引用实例,可省略参数名,仅保留类型。

第 7 章 接口

接口代表一种契约,是多个方法声明的集合。

接口最常见的使用场景,是对包外提供访问,或预留扩展空间。

我们可以先实现类型,而后再抽象出所需接口。

###执行机制

内部实现:

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。

第 8 章 并发

  • 并发:逻辑上具备同时处理多个任务的能力。
  • 并行:物理上在同一时刻执行多个并发任务。

多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。尽管协程再单个线程上通过主动切换来实现多任务, 但也有自己的优势。除了将因阻塞而浪费的时间找回来外,还免去了线程切换开销,有着不错的执行效率。 协程上运行的多个任务本质上是依旧串行的,加上可控自主调度,所以并不需要做同步处理。

用多进程来实现分布式和负载均衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的处理器资源, 用协程来提高处理器时间片利用率。

Goroutine

简单将 goroutine 归纳为协程并不合适,运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。 这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。

运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等, 可用 runtime.GOMAXPROCS 函数修改

Gosched 暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行。

Goexit 立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发 panic,也无法捕获。

如果在 main.main 里调用 Goexit,它会等待其他任务结束,然后让进程直接崩溃。

无论身处哪一层,Goexit 都能立即终止整个调用堆栈,这与 return 仅退出当前函数不同。 标准库寒素 os.Exit 可终止进程,但不会执行延迟调用。

通道

同步模式必须有配对操作的 goroutine 出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。

Clone this wiki locally