疑问

1.环境安装 2.hello world 3.基础语法,运算符、关键字、数据类型,特别的地方 数值类型 字符串的修改、Raw字符串 分组声明 itoa枚举 函数、变量作用域和导出 array slice map make和new的区别 流程控制for、switch 多返回值,_ 变参 传值与传指针 defer 函数类型,函数作为参数 Panic和Recover 保留函数,main,init 如何导入包,处理名称冲突,别名 struct定义方法,嵌套,访问字段,名称冲突 method作用,传指针及用法,method继承和重写,公有私有 interface的含义及作用,空interface,判断包含类型,嵌入 反射的含义及用法 并发,通信机制,同步 channel,close+range select,goroutine阻塞 error

环境

GOPATH 默认 %USERPROFILE%\go,用来存放Go源码,Go的可运行文件,以及相应的编译之后的包文件,这个变量不能和Go的安装目录一样。所以这个目录下面有三个子目录:src、bin、pkg 按照约定这三个目录的作用是:

src 存放你项目的源码 (xxx.go) pkg 存放编译后生成的库文件 (xxx.a) bin 存放编译后生成的可执行文件 (如果你将 $GOPATH/bin 加入到 PATH 变量中后, 则可以直接使用编译好的程序)

go env -w GOPROXY=https://goproxy.cn

这是因为你没有理解GOPATH是什么东西:

GOPATH不应该指向Go的安装路径,而是指向你的工作空间 (见 https://golang.> org/doc/code.h… )。每当你用go get或go install安装某个包时,它就会被扔> 到GOPATH里。这就是为什么它会警告你。你绝对不会希望网上安装的什么包都被扔到> 你的Go安装文件中。 也可以创建一个或多个你想用来开发Go代码的文件夹,并将其设置为GOPATH(但注> 意,如果设置了多个文件夹,那么go get将只会把包安装到第一个文件夹中)。 来源

所以,你需要另外创建个文件夹(随便放到哪里),然后把这个路径设置为GOPATH

比如我的路径:/Users/shuirong/Code/Go

go env -w 设置环境变量报错

1D:\env\Go>go version
2go version go1.22.0 windows/amd64
3
4D:\env\Go>go env -w GOPATH=E:\work\doit\go
5warning: go env -w GOPATH=... does not override conflicting OS environment variable

无法覆盖OS级别的环境变量,直接系统改

hello world

go mod init

使用go module模式新建项目时,我们需要通过go mod init 项目名命令对项目进行初始化,该命令会在项目根目录下生成go.mod文件。例如,我们使用hello作为我们第一个Go项目的名称,执行如下命令。

1go mod init hello

编写代码

接下来在该目录中创建一个main.go文件:

1package main  // 声明 main 包,表明当前是一个可执行程序
2
3import "fmt"  // 导入内置 fmt 包
4
5func main(){  // main函数,是程序执行的入口
6 fmt.Println("Hello World!")  // 在终端打印 Hello World!
7}

编译运行

go build命令表示将源代码编译成可执行文件。在hello目录下执行:

1go build

加 -x 可以查看过程?

或者在其他目录执行以下命令:

1go build hello

go编译器会去 GOPATH的src目录下查找你要编译的hello项目

编译得到的可执行文件会保存在执行编译命令的当前目录下,如果是windows平台会在当前目录下找到hello.exe可执行文件。

可在终端直接执行该hello.exe文件:

1c:\desktop\hello>hello.exe
2Hello World!

我们还可以使用-o参数来指定编译后得到的可执行文件的名字。

1go build -o heiheihei.exe

go run main.go 也可以执行程序,该命令本质上也是先编译再执行。

跨平台编译

默认我们go build的可执行文件都是当前操作系统可执行的文件,如果我想在windows下编译一个linux下可执行文件,那需要怎么做呢?

只需要指定目标操作系统的平台和处理器架构即可,例如Windows平台cmd下按如下方式指定环境变量。

1SET CGO_ENABLED=0   // 禁用CGO
2SET GOOS=linux      // 目标平台是linux
3SET GOARCH=amd64    // 目标处理器架构是amd64

注意:如果你使用的是PowerShell终端,那么设置环境变量的语法为$ENV:CGO_ENABLED=0。然后再执行go build命令,得到的就是能够在Linux平台运行的可执行文件了。

Mac 下编译 Linux 和 Windows平台 64位 可执行程序:

1CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
2CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Linux 下编译 Mac 和 Windows 平台64位可执行程序:

1CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
2CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Windows下编译Mac平台64位可执行程序:

1SET CGO_ENABLED=0
2SET GOOS=darwin
3SET GOARCH=amd64
4go build

Go程序是怎样跑起来的 介绍了GO的编译过程,词法分析、语法分析的大概逻辑和源码路径

基础语法

Go基础语法宝典

实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响 Go 代码的正确解析(译注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字 break、continue、fallthrough或 return 中的一个、运算符和分隔符 ++、–、)]} 中的一个)。举个例子,函数的左括号 { 必须和 func 函数声明在同一行上,且位于末尾,不能独占一行,而在表达式 x+y 中,可在 + 后换行,不能在 + 前换行(译注:以+结尾的话不会被插入分号分隔符,但是以 x 结尾的话则会被分号分隔符,从而导致编译错误)。

Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。

1break      default       func     interface   select
2case       defer         go       map         struct
3chan       else          goto     package     switch
4const      fallthrough   if       range       type
5continue   for           import   return      var

此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

1内建常量: true false iota nil
2内建类型: int int8 int16 int32 int64
3            uint uint8 uint16 uint32 uint64 uintptr
4            float32 float64 complex128 complex64
5            bool byte rune string error
6内建函数: make len cap new append copy close delete
7            complex real imag
8            panic recover

名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。

这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

简短声明,只能用在函数内部,在函数外部使用则会无法编译通过

1/*
2    定义三个变量,它们分别初始化为相应的值
3    vname1为v1,vname2为v2,vname3为v3
4    编译器会根据初始化的值自动推导出相应的类型
5*/
6vname1, vname2, vname3 := v1, v2, v3

_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,将值35赋予b,并同时丢弃34

1_, b := 34, 35

布尔值的类型为bool,值是truefalse,默认为false

runeint8int16int32int64byteuint8uint16uint32uint64。其中runeint32的别称,byteuint8的别称。

需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。

1var a int8
2
3var b int32
4
5c:=a + b

上述代码会产生错误:invalid operation: a + b (mismatched types int8 and int32)

另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。

浮点数的类型有float32float64两种(没有float类型),默认是float64

Go还支持复数。它的默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。下面是一个使用复数的例子:

1var c complex64 = 5+5i
2//output: (5+5i)
3fmt.Printf("Value is: %v", c)

修改字符串

1s := "hello"
2c := []byte(s)  // 将字符串 s 转换为 []byte 类型
3c[0] = 'c'
4s2 := string(c)  // 再转换回 string 类型
5fmt.Printf("%s\n", s2)

Go中可以使用+操作符来连接两个字符串:

1s := "hello,"
2m := " world"
3a := s + m
4fmt.Printf("%s\n", a)

修改字符串也可写为:

1s := "hello"
2s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
3fmt.Printf("%s\n", s)

如果要声明一个多行的字符串怎么办?可以通过```来声明:

1m := `hello
2    world`

``` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出:

1hello
2    world

分组声明

在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

例如下面的代码:

1import "fmt"
2import "os"
3const i = 100
4const pi = 3.1415
5const prefix = "Go_"
6var i int
7var pi float32
8var prefix string

可以分组写成如下形式:

 1import(
 2    "fmt"
 3    "os"
 4)
 5const(
 6    i = 100
 7    pi = 3.1415
 8    prefix = "Go_"
 9)
10var(
11    i int
12    pi float32
13    prefix string
14)

iota枚举

Go里面有一个关键字iota,这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增加一行加1:

 1package main
 2import (
 3    "fmt"
 4)
 5const (
 6    x = iota // x == 0
 7    y = iota // y == 1
 8    z = iota // z == 2
 9    w        // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
10)
11const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
12const (
13    h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
14)
15const (
16    a       = iota //a=0
17    b       = "B"
18    c       = iota             //c=2
19    d, e, f = iota, iota, iota //d=3,e=3,f=3
20    g       = iota             //g = 4
21)
22func main() {
23    fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
24}

除非被显式设置为其它值或iota,每个const分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota,则它也被设置为iota

能否导出,公有私有

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

由于长度也是数组类型的一部分,因此[3]int[4]int是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的slice类型了

注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。

slice有一些简便的操作

  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]
  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]
  • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

s := arr[startIndex:endIndex] 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。

零长切片使用方法

1func TrimSpace(s []byte) []byte {
2 b := s[:0]
3 for _, x := range s {
4  if x != ' ' {
5   b = append(b, x)
6  }
7 }
8 return b
9}

因为切片引用了整个原始数组,导致自动垃圾回收器不能及时释放底层数组的空间。一个小的需求可能导致需要长时间保存整个文件数据。这虽然这并不是传统意义上的内存泄漏,但是可能会拖慢系统的整体性能。要修复这个问题,可以将感兴趣的数据复制到一个新的切片中(数据的传值是Go语言编程的一个哲学,虽然传值有一定的代价,但是换取的好处是切断了对原始数据的依赖):

1func FindPhoneNumber(filename string) []byte {
2 b, _ := ioutil.ReadFile(filename)
3 b = regexp.MustCompile("[0-9]+").Find(b)
4 return append([]byte{}, b...)
5}

makenew操作

make用于内建类型(mapslice 和channel)的内存分配。new用于各种类型的内存分配。

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:

new返回指针。

内建函数make(T, args)new(T)有着不同的功能,make只能创建slicemapchannel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于slicemapchannel来说,make初始化了内部的数据结构,填充适当的值。

make返回初始化后的(非零)值。

import

1、点操作 有时候会看到如下的方式导入包

1import(
2    . "fmt"
3)

这个点操作的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名,也就是前面调用的fmt.Println("hello world")可以省略的写成Println("hello world")

2、别名操作 别名操作顾名思义可以把包命名成另一个用起来容易记忆的名字

1import(
2        f "fmt"
3)

别名操作的话调用包函数时前缀变成了前缀,即f.Println("hello world")

3、_操作 这个操作经常是让很多人费解的一个操作符,请看下面这个import

1import (
2    "database/sql"
3    _ "github.com/ziutek/mymysql/godrv"
4)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。

struct

 1package main
 2
 3import "fmt"
 4
 5type Human struct {
 6 name  string
 7 age   int
 8 phone string // Human类型拥有的字段
 9}
10type Employee struct {
11 Human      // 匿名字段Human
12 speciality string
13 phone      string // 雇员的phone字段
14}
15
16func main() {
17 Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
18 fmt.Println("Bob's work phone is:", Bob.phone)
19 // 如果要访问Human的phone字段
20 fmt.Println("Bob's personal phone is:", Bob.Human.phone)
21}

method

 1package main
 2import "fmt"
 3const(
 4    WHITE = iota
 5    BLACK
 6    BLUE
 7    RED
 8    YELLOW
 9)
10type Color byte
11type Box struct {
12    width, height, depth float64
13    color Color
14}
15type BoxList []Box //a slice of boxes
16func (b Box) Volume() float64 {
17    return b.width * b.height * b.depth
18}
19func (b *Box) SetColor(c Color) {
20    b.color = c
21}
22func (bl BoxList) BiggestColor() Color {
23    v := 0.00
24    k := Color(WHITE)
25    for _, b := range bl {
26        if bv := b.Volume(); bv > v {
27            v = bv
28            k = b.color
29        }
30    }
31    return k
32}
33func (bl BoxList) PaintItBlack() {
34    for i := range bl {
35        bl[i].SetColor(BLACK)
36    }
37}
38func (c Color) String() string {
39    strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
40    return strings[c]
41}
42func main() {
43    boxes := BoxList {
44        Box{4, 4, 4, RED},
45        Box{10, 10, 1, YELLOW},
46        Box{1, 1, 20, BLACK},
47        Box{10, 10, 1, BLUE},
48        Box{10, 30, 1, WHITE},
49        Box{20, 20, 20, YELLOW},
50    }
51    fmt.Printf("We have %d boxes in our set\n", len(boxes))
52    fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
53    fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
54    fmt.Println("The biggest one is", boxes.BiggestColor().String())
55    fmt.Println("Let's paint them all black")
56    boxes.PaintItBlack()
57    fmt.Println("The color of the second one is", boxes[1].color.String())
58    fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
59}

指针作为receiver

现在让回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,可以使用*Box。

定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以需要传入指针。

这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解

这里也许会问SetColor函数里面应该这样定义*b.Color=c,而不是b.Color=c,需要读取到指针相应的值。

其实Go里面这两种方式都是正确的,当用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道要通过指针去获取这个值。PaintItBlack里面调用SetColor的时候是不是应该写成(&bl[i]).SetColor(BLACK),因为SetColor的receiver是*Box,而不是Box。这两种方式都可以,因为Go知道receiver是指针,他自动转了。

也就是说:

如果一个method的receiver是*T,可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method

类似的

如果一个method的receiver是T,可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method

所以不用担心是调用的指针的method还是不是指针的method,Go知道要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。

element.(type)

Comma-ok断言

 1package main
 2import (
 3    "fmt"
 4    "strconv"
 5)
 6type Element interface{}
 7type List [] Element
 8type Person struct {
 9    name string
10    age int
11}
12//定义了String方法,实现了fmt.Stringer
13func (p Person) String() string {
14    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
15}
16func main() {
17    list := make(List, 3)
18    list[0] = 1 // an int
19    list[1] = "Hello" // a string
20    list[2] = Person{"Dennis", 70}
21    for index, element := range list {
22        if value, ok := element.(int); ok {
23            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
24        } else if value, ok := element.(string); ok {
25            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
26        } else if value, ok := element.(Person); ok {
27            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
28        } else {
29            fmt.Printf("list[%d] is of a different type\n", index)
30        }
31    }
32}

反射

laws of reflection

反射还不太清楚,需要看上面这个链接,这个链接还没看

并发

设计上要遵循:不要通过共享来通信,而要通过通信来共享。

 1package main
 2
 3import (
 4 "fmt"
 5 "runtime"
 6)
 7
 8func say(s string) {
 9 for i := 0; i < 5; i++ {
10  runtime.Gosched()
11  fmt.Println(s)
12 }
13}
14func main() {
15 go say("world") //开一个新的Goroutines执行
16 say("hello")    //当前Goroutines执行
17}

Channel

默认情况下,channel接收和发送数据都是阻塞的

 1package main
 2
 3import "fmt"
 4
 5func sum(a []int, c chan int) {
 6 total := 0
 7 for _, v := range a {
 8  total += v
 9 }
10 c <- total // send total to c
11}
12func main() {
13 a := []int{7, 2, 8, -9, 4, 0}
14 c := make(chan int)
15 go sum(a[:len(a)/2], c)
16 go sum(a[len(a)/2:], c)
17 x, y := <-c, <-c // receive from c
18 fmt.Println(x, y, x+y)
19}
 1package main
 2
 3import (
 4 "fmt"
 5)
 6
 7func fibonacci(n int, c chan int) {
 8 x, y := 1, 1
 9 for i := 0; i < n; i++ {
10  c <- x
11  x, y = y, x+y
12 }
13 close(c)
14}
15func main() {
16 c := make(chan int, 10)
17 go fibonacci(cap(c), c)
18 for i := range c {
19  fmt.Println(i)
20 }
21}

生产者通过内置函数close关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch测试channel是否被关闭

error

函数传参

好文章 https://www.piglei.com/articles/go-func-argument-patterns/

使用接口实现“函数式选项” 首先,定义一个名为 Option 的接口类型,其中仅包含一个方法 applyTo:

1type Option interface {
2    applyTo(*ListAppsOptions)
3}

然后,把这批 With* 工厂函数改为各自的自定义类型,并实现 Option 接口:

 1type WithPager struct {
 2    limit  int
 3    offset int
 4}
 5
 6func (r WithPager) applyTo(opts *ListAppsOptions) {
 7    opts.limit = r.limit
 8    opts.offset = r.offset
 9}
10
11type WithOwner string
12
13func (r WithOwner) applyTo(opts *ListAppsOptions) {
14    opts.owner = string(r)
15}
16
17type WithHasDeployed bool
18
19func (r WithHasDeployed) applyTo(opts *ListAppsOptions) {
20    val := bool(r)
21    opts.hasDeployed = &val
22}

做完这些准备工作后,查询函数也要做出相应的调整:

 1// ListApplications 查询应用列表,使用可变参数,Option 接口类型
 2func ListApplications(opts ...Option) []Application {
 3    config := ListAppsOptions{limit: 10, offset: 0, owner: "", hasDeployed: nil}
 4    for _, opt := range opts {
 5        // 调整调用方式
 6        opt.applyTo(&config)
 7    }
 8    // ...
 9    return allApps[config.offset : config.offset+config.limit]
10}

调用代码和之前类似,如下所示:

1ListApplications(WithPager{limit: 2, offset: 5}, WithOwner("piglei"))
2ListApplications(WithOwner("piglei"), WithHasDeployed(false))

各个可选项从工厂函数变成 Option 接口后,找出所有可选项变得更方便了,使用 IDE 的“查找接口的实现”就可以轻松完成任务。

GC

三色标记算法将程序中的对象分成白色、黑色和灰色三类:

白色对象:潜在的垃圾,其内存可能会被垃圾收集器回收; 黑色对象:活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象; 灰色对象:活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;

Go 语言在 v1.8 组合 Dijkstra 插入写屏障和 Yuasa 删除写屏障构成混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色。

增量式(Incremental)的垃圾收集,并发(Concurrent)的垃圾收集

GC时机 1、gcTriggerHeap :堆内存的分配达到达控制器计算的触发堆大小; 2、gcTriggerTime :如果一定时间内没有触发,就会触发新的循环,该出发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟; 3、gcTriggerCycle:如果当前没有开启垃圾收集,则触发新的循环; 4、runtime.gcpercent 是触发垃圾收集的内存增长百分比,默认情况下为 100,即堆内存相比上次垃圾收集增长 100% 时应该触发 GC,并行的垃圾收集器会在到达该目标前完成垃圾收集。

用于开启垃圾收集的方法 runtime.gcStart 会接收一个 runtime.gcTrigger 类型的结构,所有出现 runtime.gcTrigger 结构体的位置都是触发垃圾收集的代码:

runtime.sysmon 和 runtime.forcegchelper :后台运行定时检查和垃圾收集; runtime.GC :用户程序手动触发垃圾收集; runtime.mallocgc :申请内存时根据堆大小触发垃圾收集。

内存管理

Go 语言的内存管理是参考 tcmalloc 实现的

直接调用 mmap 申请内存,会有一定的代价。比如:

1、系统调用会导致程序进入内核态,内核分配完内存后(也就是上篇所讲的,对虚拟地址和物理地址进行映射等操作),再返回到用户态。 2、频繁申请很小的内存空间,容易出现大量内存碎片,增大操作系统整理碎片的压力。 3、为了保证内存访问具有良好的局部性,开发者需要投入大量的精力去做优化,这是一个很重的负担。

内存池

将有限的计算资源布局成金字塔结构,再将数据从热到冷分为几个层级,放置在金字塔结构上。调度器不断做调整,将热数据放在金字塔顶层,冷数据放在金字塔底层。 这种设计利用了计算的局部性特征,认为冷热数据的交替是缓慢的。所以最怕的就是,数据访问出现冷热骤变。在操作系统上我们称这种现象为内存颠簸,系统架构上通常被说成是缓存穿透。其实都是一个意思,就是过度的使用了金字塔低端的资源。

这套内部机制,使得开发高性能服务容易很多,通俗来讲就是坑少了。一般情况下你随便写写性能都不会太差。我遇到过的导致内存分配出现压力的主要有 2 中情况:

1、频繁申请大对象,常见于文本处理,比如写一个海量日志分析的服务,很多日志内容都很长。这种情况建议自己维护一个对象([]byte)池,避免每次都要去 mheap 上分配。 2、滥用指针,指针的存在不仅容易造成内存浪费,对 GC 也会造成额外的压力,所以尽量不要使用指针。

内存碎片 内存碎片是系统在内存管理过程中,会不可避免的出现一块块无法被使用的内存空间,这是内存管理的产物。

内部碎片 一般都是因为字节对齐,如上面介绍 Tiny 对象分配的部分;为了字节对齐,会导致一部分内存空间直接被放弃掉,不做分配使用。 再比如申请 28B 大小的内存空间,系统会分配 32B 的空间给它,这也导致了其中 4B 空间是被浪费掉的。这就是内部碎片。

外部碎片 一般是因为内存的不断分配释放,导致一些释放的小内存块分散在内存各处,无法被用以分配。如图:

img

上面的 8B 和 16B 的小空间,很难再被利用起来。不过 Go 的内存管理机制不会引起大量外部碎片。

其它

在 main.main 函数执行之前所有代码都运行在同一个goroutine,也 就是程序的主系统线程中。因此,如果某个 init 函数内部用go关键字启动了新的 goroutine的话,新的goroutine只有在进入 main.main 函数之后才可能被执行到。

a… 解包。当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果

虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要明白Go语言中指针不再是固定不变的了(因此不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址)。

看看下面这个例子:

1func f(x int) *int {
2 return &x
3}
4
5func g() int {
6 x := new(int)
7 return *x
8}

第一个函数直接返回了函数参数变量的地址——这似乎是不可以的,因为如果参数 变量在栈上的话,函数返回之后栈变量就失效了,返回的地址自然也应该失效了。 但是Go语言的编译器和运行时比我们聪明的多,它会保证指针指向的变量在合适的 地方。第二个函数,内部虽然调用 new 函数创建了 *int 类型的指针对象,但是 依然不知道它具体保存在哪里。对于有C/C++编程经验的程序员需要强调的是:不 用关心Go语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;同样不要假设 变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变 化的时候。

方法表达式、方法值

 1// 不依赖具体的文件对象
 2// func CloseFile(f *File) error
 3var CloseFile = (*File).Close
 4
 5// 不依赖具体的文件对象
 6// func ReadFile(f *File, offset int64, data []byte) int
 7var ReadFile = (*File).Read
 8
 9// 文件处理
10f, _ := OpenFile("foo.dat")
11ReadFile(f, 0, data)
12CloseFile(f)

接口

可以通过定制自己的输出对象,将每个字符转为大写字符后输出:

 1type UpperWriter struct {
 2 io.Writer
 3}
 4
 5func (p *UpperWriter) Write(data []byte) (n int, err error) {
 6 return p.Writer.Write(bytes.ToUpper(data))
 7}
 8func main() {
 9 fmt.Fprintln(&UpperWriter{os.Stdout}, "hello, world")
10}
 1type UpperString string
 2
 3func (s UpperString) String() string {
 4 return strings.ToUpper(string(s))
 5}
 6
 7// type fmt.Stringer interface {
 8//  String() string
 9// }
10
11func main() {
12 fmt.Fprintln(os.Stdout, UpperString("hello, world"))
13}

再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。

1type testing.TB interface {
2    Error(args ...interface{})
3    Errorf(format string, args ...interface{})
4    ...
5    // A private method to prevent users implementing the
6    // interface and so future additions to it will not
7    // violate Go 1 compatibility.
8    private()
9}

通过嵌入匿名接口或嵌入匿名指针对象来实现继承的做法其实是一种纯虚继承,我们继承的只是接口指定的规范,真正的实现在运行的时候才被注入

 1package main
 2
 3import (
 4 "fmt"
 5 "testing"
 6)
 7
 8type TB struct {
 9 testing.TB
10}
11
12func (p *TB) Fatal(args ...interface{}) {
13 fmt.Println("TB.Fatal disabled!")
14}
15func main() {
16 var tb testing.TB = new(TB)
17 tb.Fatal("Hello, playground")
18}

协程

没有在任务开始前调用Add方法添加任务,而是在任务开始之后再调用Add方法添加任务。

这可能会导致某些任务未被加入到WaitGroup中,等待协程就调用了wg.Wait方法,这样就会导致一些任务未被加入WaitGrou,从而导致等待协程不会等待这些任务执行完成。如果这种情况发生了,我们会看到"All goroutines finished"被输出,但实际上有一些协程还没有完成。

因此,我们应该在所有任务添加完毕之后再调用Wait方法,以保证等待的正确性。

题目

a constant index that is untyped is given type int

 1package main
 2
 3import (
 4 "fmt"
 5)
 6
 7func main() {
 8 m := [...]int{
 9  'a': 1,
10  'b': 2,
11  'c': 3,
12 }
13 m['a'] = 3
14 fmt.Println(len(m)) // 100
15}

未整理

还有个相关的工具:goimports,可以根据代码需要,自动地添加或删除 import 声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:

1go get golang.org/x/tools/cmd/goimports

自增语句 i++ 给 i 加 1;这和 i+=1 以及 i=i+1 都是等价的。对应的还有 i– 给 i 减 1。它们是语句,而不像 C 系的其它语言那样是表达式。所以 j=i++ 非法,而且 ++ 和 – 都只能放在变量名后面,因此 –i 也非法。

可以在 https://golang.org/pkghttps://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。

1$ go doc http.ListenAndServe
2package http // import "net/http"
3func ListenAndServe(addr string, handler Handler) error
4    ListenAndServe listens on the TCP network address addr and then
5    calls Serve with handler to handle requests on incoming connections.
6...

锁的地址才是区分锁的因素 noCopy通过代码检查工具进行提示,包含了Lock Unlock方法 race并发数据竞争检测工具 go run -race,关注同步原语操作