Golang可信编程知识与应用

Posted by xnchen on September 6, 2021

[TOC]

参考

  1. effective Go
  2. go语言高性能编程
  3. Go语言101

  4. 原题1

知识点

slice

案例

  • 可以通过内置函数make创建。

  • 也可以通过Slice expressions(切片表达式)从数组创建,这种情况下创建的slice和数组共享底层数组。

    • 相当于两个指针指向同一个地址,此时对slice元素的修改,会影响到共享的数组或slice。

    • 对slice进行append等操作时,可能会造成slice的自动扩容,扩容时会重新申请一个底层数组,该slice底层指针会指向新的地址,如果该slice之前和其他数组共享底层数组,此时两个指针就指向不同的地址,不再共享底层数组了。

      • 扩容规则:

        如果新的大小是当前大小2倍以上,则大小增长为新大小

        如果当前大小小于1024,按每次2倍增长,否则循环以下操作:每次按当前大小1/4增长,直到增长的大小超过或者等于新大小

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[2:4:6] // data[low, high, max]

其中,low和high决定了切片起始和结束的下标:len = high - low

low和max决定了切片的cap:cap = max - low

G-P-M模型

资料

G: 代表一个goroutine对象,每次go调用的时候,都会创建一个G对象,它包括栈、指令指针以及对于调用goroutines很重要的其它信息,比如阻塞它的任何channel,其主要数据结构:

M:代表一个线程,每次创建一个M的时候,都会有一个底层线程创建;所有的G任务,最终还是在M上执行,其主要数据结构:

P:代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在么一个CPU核上执行一样,由P来调度G在M上的运行,P的个数就是GOMAXPROCS(最大256),启动时固定的,一般不修改;M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000);每一个P保存着本地G任务队列,也有一个全局G任务队列。P的数据结构:

加密算法

禁止使用私有算法或者弱加密算法(如DES、MD5、SHA1等)

给予哈希算法的口令安全存储必须加入盐值。

口令单向哈希可以使用PBKDF2算法。

方法表达式

案例

具体类型实例变量直接调用其方法时,编译器进行自动转换,即使接收者是指针的方法,仍然可以使用值类型变量进行调用;但通过表达式调用时,编译器不会进行自动转换,会进行严格的方法集检查。

方法表达式相当于提供一种语法将类型方法调用显式地转换为函数调用,接收者( receiver )必须显式地传递进去。下面定义一个类型 T ,增加两个方法,方法 Get 的接收者为 T ,方法 Set 的接收者类型为 *T

package main

import "fmt"

type T struct {
	a int
}

func (t T) Get() int {
	return t.a
}

func (t *T) Set(i int) int {
	t.a = i
	return t.a
}

func (t *T) Print() {
	fmt.Printf("%p, %v, %d\n", t, t, t.a)
}

表达式 T.Get (*T).Set 被称为方法表达式(method expression),方法表达式可以看作函数名,只不过这个函数的首个参数是接收者的实例或指针。T.Get 的函数签名是 func (t T) int(*T).Set 的函数签名是 func( t *T, i int)

注意: 这里的 T.Get不能写成 (*T).Get(*T).Set也不能写成 T.Set ,在方法表达式中编译器不会做自动转换。

defer

案例

执行顺序:栈,先进后出

defer和return:return先执行,defer后执行

命名返回值遇到defer

func DeferFunc() (t int) {
    defer func() {t*=10}()

	return 2
}

return将t的值变为2,然后再执行defer对应的函数,然后defer的语句还会改变t的值,最后该函数返回值会变成20

遇到panic时:执行defer,结束后向stderr抛出panic信息。如果遇到recover,则停止panic,返回recover处继续往下执行

defer下的函数参数包含子函数时:defer的函数入参需要进栈,所以声明defer就要执行子函数。

defer中包含panic

除法panic后,defer顺序出栈执行,如果defer中含有panic,将会覆盖之前的异常panic信息,被异常捕获获得时只会打印最后一个panic。

runtime包

版本:runtime.Version() 现存协程数:runtime.NumGoroutine() 当前进程可用的逻辑CPU数量:runtime.NumCPU() 设置GOMAXPROCS:runtime.GOMAXPROCS(m)

关于函数调用信息

// 返回当前goroutine的栈上的函数调用信息,主要有当前pc值、调用文件、行号等信息
// skip是要跳过的栈帧数,如果为0代表runtime.Caller的调用函数本身
func Caller(skip int) (pc uintptr, file string, line int, ok bool)

// 把调用它的函数go程序栈上的程序计数器列表填入pc,返回值是程序计数器个数
// skip=0表示Callers自身,为1表示调用函数。
func Callers(skip int, pc []uintptr) int

// 返回给定pc对应的函数,如果是无效pc则返回nil
func FuncForPc(pc uintptr) *Func

让出当前goroutine占用的cpu,让其他goroutine获得执行的机会:runtime.Gosched()

在GOMAXPROCS没有被设定的时候,go只拥有一个线程。执行线程的时候需要使用某些并发原语才能切换执行内容。runtiume.Gosched()就是用来做这个的。当Gosched被调用的时候,调度器会切换到另一个goroutine中。

` time.Sleep()暂停goroutine,runtime.Gosched()`可以让其他goroutine有机会被执行,当前Goroutine仍有可能会继续执行,在main函数返回前,新的goroutine可能没有机会执行。

runtime.Goexit()会终止当前的goroutine但不影响其他goroutine,直到在所有其他goroutine退出后,程序才异常退出。

import

import的包按照稳定度排序(标准库,系统库,第三方库,本项目库)

特殊形式

  1. 加下划线: 有些时候我们并不需要把整个包都导入进来,仅仅是希望它执行init()函数而已。这个时候就可以使用import _引用该包,即[import _ 包路径]只是引用该包,仅仅是为了调用init()函数,所以无法通过报名来调用包中的其他函数。

  2. 加点:

    import和引用的包名之间加点(.)操作的含义就是这个包导入之后再调用这个包的函数时,可以省略前缀的包名:

    import . "fmt"
    func main() {
        Println("Hello world")
       
    
  3. 别名:

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

    import f "fmt"
    func main() {
        f.Println("Hello world")
    }
    

os/exec包

简明教程

cmd := exec.Command("ls", "-lah")
err := cmd.Run()
if err != nil {
	log.Fatalf("failed to call cmd.Run(): %v", err)
}

exec.Command()返回一个*Cmd的对象,代表一个待执行的外部命令。

cmd.Run()cmd.Output()方法中都封装了cmd.Start()方法

go语言格式化工具

gofmt:官方代码格式化工具

goimport:相比gofmt加入了import代码块的格式化

goreturns:相比gofmt加入了return代码的修复功能

注意:gotype是类型检查而不是格式化工具

sync/atomic包

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

简明教程

sync/atomic包支持的原子操作有:

  • 加法(add)
  • 比较并交换(compare and swap)
  • 加载(load)
  • 存储(store)
  • 交换(swap)

B. sync/atomic包中原子操作支持的数据类型包括:int32、int64、uint32、uint64、uintptr、unsafe.Pointer、atomic.Value

net/http包中的Server类型

Close方法会立即关闭所有活动的net.Listeners以及状态位StateNew,StateActive或StateIdle中的所有连接

Close方法不会关闭任何被劫持的连接。

Shutdown方法会在不干扰任何活跃连接的情况下关闭服务器。首先,它会关闭所有开着的监听器,然后关闭所有空闲连接,接着无限等待所有连接变成空闲状态,最后关闭

Shutdown方法不会尝试关闭或等待被劫持的连接。

unsafe包

简明教程

unsafe包可以绕过系统,直接操作内存。

type ArbitraryType int

// Pointer类型可以指向任何类型,类似C中的void*
type Pointer *ArbitraryType

type pointer uintptr

任何类型的指针*T都可以转换成unsafe.Pointer。

unsafe.Pointer可以转换为uintptr,对uintptr可以进行数学运算。

uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

转换成uintptr后,加减都是字节长度(和c不一样,uintptr不自动匹配数据类型大小了)

unsafe.sizeof():得到数据类型的大小

其中,string类型的数据结构:

type StringHeader struct { 
    Data uintptr 
    Len  int
}

在64位系统上,uintptr和int都是八位,所以unsafe.sizeof(str)的结果是16

使用chan实现互斥锁

两种实现互斥锁的方法

两种实现方法:

  1. chan的cap为1,放入一个元素代表获得锁,谁获得了这个元素则获取到锁。
  2. chan的cap为1,空槽为锁,能成功发送到chan的则获取到了锁。

简单的例子

命名约定

  1. 文件名必须是小写单词,允许下划线组合

  2. 目录名必须全为小写单词,允许中划线组合,头尾不能为中划线
  3. 包名必须全为小写单词,无下划线,不与标准库重名
    1. 包名尽量与目录名一致
  4. 方法接收名尽量简洁,一两个字母即可,首字母小写
  5. 返回值尽量不使用命名返回值

注释

每个包(package)都必须要有包注释

包内所有导出符号都应有注释

不要出现连续星号、连续斜杠等各种为了突出显示的注释符或注释内容。

文档注释最好是完整的句子,这样它才能适应各种自动化的展示

注释的第一句应该是一句摘要信息,并且以被声明的东西开头

不可信数据

  • 文件(包括程序的配置文件)
  • 注册表
  • 网络
  • 环境变量
  • 命令行
  • 用户输入(包括命令行、界面)
  • 用户态数据(对于内核程序)
  • 进程间通信(包括管道、消息、共享内存、socket、RPC等)
  • 函数参数(对于API)
  • 全局变量(在本函数内,其他线程会修改全局变量)

接口检查

多数接口的类型转换和检查都是在编译阶段静态完成的。例如,将一个*os.File类型传入一个接受io.Reader类型参数的函数时,只有在*os.File实现了io.Reader接口时,才能编译通过。

但是,也有一些接口检查是发生在运行时的。其中一个例子来自encoding/json包内定义的Marshaler接口。

接口

effective go

如果一个类型只是用来实现接口,并且除了该接口以外没有其它被导出的方法,那就不需要导出这个类型。只导出接口,清楚地表明了其重要的是行为,而不是实现,并且其它具有不同属性的实现可以反映原始类型的行为。这也避免了对每个公共方法实例进行重复的文档介绍。

这种情况下,构造器应该返回一个接口值,而不是所实现的类型。

由于几乎任何事物都可以附加上方法,所以几乎任何事物都能够满足接口的要求。

Go 的类型系统限制了哪些类型可以作为接收器. 它不能是接口类型或指针,因此它不可能为一个空接口(interface{})定义方法来满足所有类型。 只允许使用类型名称,所以,类型字面值会导致编译错误:

func (v map[string]float64) M() {}

错误信息 invalid receiver type map[string]float64 (map[string]float64 is an unnamed type).

内嵌

Go没有提供经典的类型驱动式的派生类概念,但却可以通过内嵌其他类型或接口代码的方式来实现类似的功能。

接口只能“内嵌”接口类型。

类型

类型转换(显式转换):a(b),将b转换为a。用来在类型不同但互相兼容的类型之间的相互转换。如果不兼容就无法转换,会编译报错。

//这个转换时类型不同,也不兼容,所以编译报错
s := "ab"
i := int(s)

//这个转换类型不同,但兼容,所以OK
var j int8 = 1
m := int(j)

类型断言(隐式转换):a.(b),本质也是类型转换,在接口之间进行。断言能否成功,取决于a的动态类型是否符合b的要求。

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果类型断言失败,str仍然存在,为字符串,不过其为零值,一个空字符串。

类型别名类型定义

// 类型定义
type X string	
// 类型别名
type Y = string

定义类型:在某个类型定义声明中定义的类型

非定义类型:组合类型(指针、结构体、函数、容器(数组、切片、映射)、通道、接口)

底层类型:在溯源过程中遇到的第一个内置类型或者非定义类型(组合)

类型转换

  1. 显示类型转换规则(类似于string(s)这样,使用typeName(object)语法显式转换)
    • 如果两个
  2. 隐式类型转换规则
    • 隐式可以转换的,显式也可以
    • 接口实现相关的类型转换:如果Tx类型的值x实现了接口B,则x可以被隐式转换为类型B

init函数

golang程序初始化先于main函数执行,由runtime进行初始化,初始化顺序如下:

  1. 初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似,参见golang变量的初始化);
  2. 初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化,参见golang变量的初始化);
  3. 执行包的init函数;

init函数在同一个包内,同一个文件内都可以定义多个init函数,且在包依赖情况时,会优先执行依赖包的init函数

maininit函数在定义时都不能有任何的参数和返回值,且go程序自动调用。

init可以应用于任何包中,且可以重复定义多个。

main函数只能用于main包,且只能定义一个

channel

无缓冲的channel是同步的,有缓冲的channel是非同步的

//比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
var resChan = make(chan int)
// do request
func test() {
	select {
	case data := <-resChan:
		doData(data)
	case <-time.After(time.Second * 3):
		fmt.Println("request time out")
	}
}

for…range可以用于处理channel,range c产生的值为channel发送的值,它会迭代到channel被关闭。如果channel没有发送消息,则会一直阻塞在for…range

for i := range c{
    fmt.Println(i)
}

指针

一个指针值不能和其它任一指针类型的值进行比较

Go指针值是支持(使用比较运算符==!=)比较的。 但是,两个指针只有在下列任一条件被满足的时候才可以比较:

  1. 这两个指针的类型相同。
  2. 其中一个指针可以被隐式转换为另一个指针的类型。换句话说,这两个指针的类型的底层类型必须一致并且其中一个指针类型为非定义的(考虑结构体字段的标签)。
  3. 其中一个并且只有一个指针用类型不确定的nil标识符表示。

反射

在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

它的本质是程序在运行期探知对象的类型信息和内存结构。

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

根据 Go 官方关于反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一条是最基本的:反射是一种检测存储在 interface 中的类型和值机制。这可以通过 TypeOf 函数和 ValueOf 函数得到。

第二条实际上和第一条是相反的机制,它将 ValueOf 的返回值通过 Interface() 函数反向转变成 interface 变量。

前两条就是说 接口型变量反射类型对象 可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Typereflect.Value

第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

关于第三条,记住一句话:如果想要操作原变量,反射变量 Value 必须要 hold 住原变量的地址才行。

反射实际上是在对象类型<->Interface类型<->Value类型之间进行转换。

golang-bidirectional-reflection

如何修改变量x:=2的值:

// 获得变量x的地址的Value类型,这个类型中包含了变量x的类型和指向原x的指针等
ptr := reflect.ValueOf(&x)
// 获得原值的指针(Value类型)
ptrm := ptr.Elem()

// 更新该变量的值 
// reflect.Value.Set()的入参是Value类型,因此需要使用reflect.ValueOf()将数字转换成Value对象
ptrm.Set(reflect.ValueOf(4))
// 或者直接使用x.SetInt()
ptrm.SetInt(4)
// 或者将Value对象转换成int指针再修改
ptrx := p.Addr().Interface().(*int)
*ptrx = 3

内存对齐

func Test() {
	a:= struct {}{}
	b:= struct {
		FA float32
		FB string
	}{0,"bar"}
	c:= struct {
		FC int8
		FA float32
		FB string
		FD int64
	}{0, 0, "111", 1}
	fmt.Println(unsafe.Sizeof(a)) 	// 0
	fmt.Println(unsafe.Sizeof(b))	// 24
	fmt.Println(unsafe.Sizeof(c))	// 32
}

解释

对于struct对象x中每一个字段f,首先计算字段f的unsafe.Alignof(x.f)(类型对齐值)。

  • 对于任意类型x,unsafe.Alignof(x)至少为1
  • 其实应该就是该类型需要占据的内存大小?

  • 对齐保证要求:

    image-20210908154617441

以上述c为例,FC的对齐值=1。FA对齐值=4,偏移量需要是4的倍数,因此从第四个位置开始占据4个字节。

FB对齐值=8(string结构包括了data和uintptr两个对象,每个对象大小8字节),因此从第8个位置开始占据16个字节。

FD对齐值=8,从第24个位置开始占据8个字节。

因此c的总大小为32

string和slice的内存分配

Go中的slice和string都是类似结构的Header:

reflect.SliceHeader :

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

reflect.StringHeader :

type StringHeader struct {
        Data uintptr
        Len  int
}

unsafe.Sizeof()报告的是这些header的大小(Slice就是24,string就是16)。

要获得某个任意值的实际(“递归”)大小,请使用 Go 的内置测试和基准测试框架。

文件权限

// func OpenFile(name string, flag int, perm FileMode) (*File, error) 
// 第一个参数为文件路径,第二个参数为参数控制的文件打开方式,第三个为权限位。
os.OpenFile("xxx.txt",os.O_CREATE|os.O_WRONLY|os.O_TRUNC,0600)

// func MkdirAll(path string, perm FileMode) error
// MkdirAll 用于创建path路径及其父路径,第二个参数是权限位。
os.MkdirAll("xxx",os.ModePerm)

权限位666:

该文件拥有者对该文件拥有读写的权限但是没有操作的权限 该文件拥有者所在组的其他成员对该文件拥有读写的权限但是没有操作的权限 其他用户组的成员对该文件也拥有读写权限但是没有操作的权限

权限位777:

该文件拥有者对该文件拥有读写操作的权限 该文件拥有者所在组的其他成员对该文件拥有读写操作的权限 其他用户组的成员对该文件也拥有读写操作权限

闭包

a closure is a record storing a function together with an environment. 闭包是由函数和与其相关的引用环境组合而成的实体 。

闭包的延迟绑定

闭包保存/记录了它产生时的外部函数的所有环境。在执行这个闭包的时候,会去外部环境寻找最新的数值。

func foo0() func() {
    x := 1
    f := func() {
        fmt.Printf("foo0 val = %d\n", x)
    }
    x = 11
    return f
}

foo0()() // 猜猜我会输出什么?
func foo7(x int) []func() {
    var fs []func()
    values := []int{1, 2, 3, 5}
    for _, val := range values {
        fs = append(fs, func() {
            fmt.Printf("foo7 val = %d\n", x+val)
        })
    }
    return fs
}
// Q4实验:
f7s := foo7(11)
for _, f7 := range f7s {
    f7()
}
// 输出
// foo7 val = 16
// foo7 val = 16
// foo7 val = 16
// foo7 val = 16

goroutine的延迟绑定

func foo5() {
    values := []int{1, 2, 3, 5}
    for _, val := range values {
        go func() {
            fmt.Printf("foo5 val = %v\n", val)
        }()
    }
}

foo5()
//foo3 val = 5
//foo3 val = 5
//foo3 val = 5
//foo3 val = 5

这段匿名函数的对象就是闭包。在我们调用go func() { xxx }()的时候,只要没有真正开始执行这段代码,那它还只是一段函数声明。而在这段匿名函数被执行的时候,才是内部变量寻找真正赋值的时候。

闭包真正被执行的时候,for-loop结束了,但是val的生命周期在闭包内部被延长了且被赋值到最新的数值5。

var b=0

func test() func(int) int{
   a:=0
   return func(i int) int {
      a++
      b++
      return i+a+b
   }
}

func main() {
   f:=test()
   f2:=test()
   fmt.Println(f(1)) //3
   fmt.Println(f2(1))//4
   fmt.Println(f(1)) //6
   fmt.Println(f2(1))//7
}

这些问题都是发生在闭包使用其上下文中环境信息的时候。如果在闭包的入参中就定义该值,就不会出现这个问题。

锁:RWMutex与Mutex

对一个已经Lock的锁L,执行Lock操作会阻塞直到L被Unlock。

锁在传递给函数的时候需要传递指针。

RWMutx-读写锁

  • RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁
  • 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁
  • 写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占
  • 适用于读多写少的场景
  • RLock() 加读锁,RUnlock() 解读锁,Lock() 加写锁,Unlock() 解写锁

sysmom

1.回收闲置超过5分钟的span物理内存

2.如果超过2分钟没有垃圾回收,强制执行

3.向长时间运行的G任务发出抢占调度

4.收回因syscall长时间阻塞的P

5.将长时间未处理的netpoll结果添加到任务队列

go vet和go tool vet

教程

命令go vet是一个用于检查Go语言源码中静态错误的简单工具。

go vet命令是go tool vet命令的简单封装。它会首先载入和分析指定的代码包,并把指定代码包中的所有Go语言源码文件和以“.s”结尾的文件的相对路径作为参数传递给go tool vet命令。其中,以“.s”结尾的文件是汇编语言的源码文件。如果go vet命令的参数是Go语言源码文件的路径,则会直接将这些参数传递给go tool vet命令。

go vet可以使用绝对路径/相对路径或相对于GOPATH的路径指定检测的包

如果标记-unreachable有效(标记值不为false),那么命令程序会在函数或方法定义中查找死代码。死代码就是永远不会被访问到的代码。

image-20210910150138063

数组的传递

以下哪些会改变参数在外部的值

func Func1(a [5]int){
    a = xxx
}
 
 
func Func2(a *[5]int){
    a = xxx
}
 
func Func3(a []int){
    a = xxx
}

Go语言中函数的参数有两种传递方式,按值传递和按引用传递。

Func1的入参是数组,Go默认使用按值传递来传递参数,也就是传递参数的副本。在函数中对副本的值进行更改操作时,不会影响到原来的变量。

Go语言中,在函数调用时,引用类型(slice、map、interface、channel)都默认使用引用传递。

Func2入参是数组指针,默认使用引用传递

Func3入参是slice,引用传递

数组大小

s := "hello你好"
fmt.Println(len(s), len([]rune(s))) // 11 7

golang的字符称为rune,等价于C中的char,可直接与整数转换

len是字符串中的字节长度(英文1B,中文3B),而rune是实际表示的文字的个数 (无论是英文还是中文都是1)

map

应确保在使用对象前,对象被有效构建并初始化好。不对未构建和初始化好的对象,调用其操作方法,以确保代码的控制逻辑中不存在导致引用空指针或使用未初始化对象的缺陷。

map对象是此规则的一个明确的实例。Go语言中,对于非引用类型,声明即被默认初始化;对于引用类型,声明后其值是空值nil。因此,对slice、chan、map引用类型的对象,在访问对象前,必须使用make显式初始化(对值为nil的slice进行append操作除外,此操作将产生一个新的slice)。

pprof包

教程

Go 语言自带的 pprof 库可以分析程序的运行情况,并且提供可视化的功能。

可以做什么

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
  • Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

垃圾回收机制

教程

Go 语言在 v1.5 中引入了并发的垃圾收集器,该垃圾收集器使用了我们上面提到的三色抽象和写屏障技术保证垃圾收集器执行的正确性。

go 1.5开始采用三色标记法,go的stw1做拉齐,暂停所有goroutine,然后goroutine会恢复和内存标记并行执行,直到标记完成,然后stw2暂停所有goroutine,扫描并发标记期间新增的内存;不是整个标记期间程序都暂停。

tri-color-mark-sweep

  1. 从灰色对象的集合中选择一个灰色对象并将其标记成黑色;
  2. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;
  3. 重复上述两个步骤直到对象图中不存在灰色对象;

并发安全

Go语言里面的大多数数据结构,如map和slice都是并发不安全的。在并发的共享数据模型下,可使用外部的锁或者对象内置并发安全机制,以确保并发安全。也可使用sync.Map,即同步的Map,其内部的操作也是基于锁,可满足Map对象在并发操作的安全需要。一般要求保证同一时间只有一个gorutine来写这个slice或者map。建议尽量不要对map并发进行读写、或者同时写的操作(所有会对对象的结构进行修改的操作,如写入元素)。如果无法避免,必须要这样使用,对它进行操作之前一定要先加锁,或直接使用加锁版本的sync.Map。对于go 1.9以后的版本建议并发使用map时使用sync.Map,而不是简单的对map加锁,正常情况下sync.Map性能更好;go 1.9之前的版本并发map只能使用锁。

sync.WaitGroup

官方文档对 WaitGroup 的描述是:一个 WaitGroup 对象可以等待一组协程结束。使用方法是:

  1. main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程;
  2. worker协程执行结束以后,都要调用 wg.Done()
  3. main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回。
// src/cmd/compile/internal/ssa/gen/main.go
func  main() {
  // 省略部分代码 ...
  var wg sync.WaitGroup
  for _, task := range tasks {
    task  := task
    wg.Add(1)
    go func() {
      task()
      wg.Done()
    }()
  }
  wg.Wait()
  // 省略部分代码...
}

cgo

Go指针指的是指向Go分配的内存的指针(例如使用&运算符或者调用new函数获取的指针)。而C指针指的是C分配的内存的指针(例如调用malloc函数获取的指针)。一个指针是Go指针还是C指针,是根据内存如何分配判断的,与指针的类型无关。

Go调用C Code时,Go传递给C Code的Go指针所指的Go Memory中不能包含任何指向Go Memory的Pointer。

C调用的Go函数不能返回指向Go分配的内存的指针。

检测控制

以上规则会在运行时动态检测,可以通过设置GODEBUG环境变量修改检测程度,默认值是GODEBUG=cgocheck=1,可以通过设置为0取消这些检测,也可以通过设置为2来提高检测标准,但这会牺牲运行的效率。

此外,也可以通过使用unsafe包来逃脱这些限制,而且C语言方面也没法使用什么特殊的机制来限制调用Go。尽管如此,如果程序打破了上面的限制,很可能会以一种无法预料的方式调用失败。

#cgo语句

import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"

上面的代码中,CFLAGS部分,-D部分定义了宏PNG_DEBUG,值为1;-I定义了头文件包含的检索目录。LDFLAGS部分,-L指定了链接时库文件检索目录,-l指定了链接时需要链接png库。

json

  1. 可以序列化为JSON的类型和限制

    • 基本数据类型和普通的结构体都可以序列化,如bool类型也是可以直接转换为json的value值。循环的数据结构不能序列化为JSON,它会导致marshal陷入死循环。
    • channelcomplex 以及函数不能被编码json字符串
  2. 只有首字母是大写的成员才可以序列化为JSON 只有可导出成员(变量首字母大写)才可以序列化为json。因成员变量note是不可导出的,故无法转成json。