目录

Go进阶

IO

os.Open() bufio.Scanner() ioutil.ReadFile()

别名

type intAlias = int  //定义别名
type newInt int      //定义新类型
巧计有等于号表示类型相同无等于号表示类型不同

range

对于切片等数据结构的遍历推荐range,更简洁

s := ""
// for i := 1; i < len(os.Args); i++ {
//   s += os.Args[i] + " "
// }

//对于不使用的变量可用_代替
for _,arg := range os.Args[1:] {
    s += arg + " "
}

//字符串的+=会将更新后的新字符串复制到s中,而旧字符串会进行垃圾回收,效率慢,可用strings包中的Join方法
s = strings.Join(os.Args[1:]," ")

交换

Go可直接交换两个值:a,b=b,a

栈变量、堆变量、返回值

栈变量堆变量

var与new声明的变量没区别,在栈中还是在堆中也不确定,主要看编译器,它会做逃逸分析(escape analysis):分析指针动态范围的方法称之为逃逸分析。当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。更简单来说,逃逸分析决定一个变量是分配在堆上还是分配在栈上。
逃逸分析这种“骚操作”把变量合理地分配到它该去的地方,“找准自己的位置”。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。
https://www.qcrao.com/2019/03/01/where-go-variables-go/

动态栈:2K至1G(g0除外)

返回值

在Go中,当函数返回局部变量时,只有当返回值是指针类型时,返回值才可作为一个“变量”使用,否则返回值仅仅是一个“字面值”;而C++中返回局部变量的值相当于返回一个临时变量
即若在Go返回一个字面值的结构体,则不能对其进行点操作

bit

交& 并| 差&^

字符串

字符串保存在只读常量区,其结构有两个字段:data(内存地址)和len(长度)

s := "hello"
s[0] = 'H' //错误,不能修改
c := s[0] //正确,可以读取
//若想要修改,可转换为slice
sli := ([]byte)(s)

s1 := "中ello"
//字符串中的原始存放类型为uint8
fmt.Printf("%T\n", s1[0]) //uint8
//在通过for range遍历访问时会自动转为uft8编码为uint32
for _, c := range s1 {
    fmt.Printf("%T", c) //uint32
}
//也可显示转换为utf8编码类型rune
rs := []rune(s1)
//fmt的%c会自动转换为utf8输出
fmt.Printf("%c", s1[0]) //中

参数传递

值传递,数组也是值传递;但切片、map、chan是“引用”
本质上切片等也是值传递,只不过这个值是指针,如一个切片s被传递到function(s)中,那么在function中对s的修改不会影响外层的实参s,但是会影响其底层数据

参数传递只有当在函数内需要修改原有变量时才需要传递指针,否则优先值传递,因为传递指针不会像C语言那样避免拷贝来节省内存,反而会造成变量逃逸,会导致对象的生命周期变长从而占用内存,还会导致原本分配到栈上的内存分配到堆上从而影响效率

slice map chan

slice

若对数组进行切片,则对slice的修改都会影响到数组底层,slice多次对同一数组切片引用的是同一数组。
slice之间测试相等应使用bytes.Equal()。
slice还可对字符串进行切片
判断是否为空应用len(s) == 0,不应该用s == nil
append(slice,e)添加元素;copy复制切片,也可用来删除元素,如删除第i个元素copy(s1[i:],s1[i+1:])

s1 := make([]int,2,10) 相比于 s1 := []int 的好处是:①若len不为0则可以在范围内通过下标访问否则刚开始只能通过append;②若cap不为0可以根据需要预先分配容量提高效率

//切片除了定义时用make指定cap外,还可在切片时指定
s2 := s1[0:2:10]   //s2有两个元素s1[0]、s1[1],容量cap为10

a = append([]int{0}, a...)        // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
a = append(a[:i], append([]int{x}, a[i:]...)...)     // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素

扩容规则:

  1. 先计算预估容量
    1. /images/go/sliceMem.png
    2. 即若申请的cap > 2*oldCap,说明扩容空间相对现有空间较大,直接扩容到新cap大小;若cap<2*oldCap,则说明扩容空间相对较小,则申请较小空间就行,若原来空间本来就小(即在1024以下),那直接翻倍就行(2*oldCap),否则,应每次在原有空间大小的基础上增加1/4进行试探,知道大小大于Cap
  2. 再到go的内存池中申请内存,若内存池中维护了8,16,32,64,128,256等大小的额内存片,而预估容量为6即6*8=48字节,则会申请64字节大小的内存片,故扩容后的容量为64/8=8大小
避免切片内存泄漏

切片操作并不会复制底层的数据。底层的数组会被保存在内存中,直到它不再被引用。但是有时候可能会因为一个小的内存引用而导致底层整个数组处于被使用的状态,这会延迟自动内存回收器对底层数组的回收。

例如,FindPhoneNumber函数加载整个文件到内存,然后搜索第一个出现的电话号码,最后结果以切片方式返回。

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

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

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

类似的问题,在删除切片元素时可能会遇到。假设切片里存放的是指针对象,那么下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,从而导致不能及时被自动垃圾回收器回收(这要依赖回收器的实现方式):

var a []*int{ ... }
a = a[:len(a)-1]    // 被删除的最后一个元素依然被引用, 可能导致GC操作被阻碍

保险的方式是先将需要自动内存回收的元素设置为nil,保证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作:

var a []*int{ ... }
a[len(a)-1] = nil // GC回收最后一个元素内存
a = a[:len(a)-1]  // 从切片删除最后一个元素

当然,如果切片存在的周期很短的话,可以不用刻意处理这个问题。因为如果切片本身已经可以被GC回收的话,切片对应的每个元素自然也就是可以被回收的了。

参考链接

map

delete(map,e)删除元素
禁止对map元素取址,如&mp1["key"]无效,原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

哈希后进行桶选择可以使用模运算(hash%m,共m个桶)或与运算(hash&(m-1),共2^m个桶,其中m必须为2的整数幂),go使用了后者
go的扩容采用了渐进式扩容。当空间不足时(通过负载因子来衡量,默认是6.5,如果超过,则翻倍扩容)或溢出桶过多时(删除了较多的数据时会发生),会重新申请一块足够大的空间进行扩容,将旧数据复制到新空间,但当数据量比较大时,复制所需时间较长,会造成性能的瞬时抖动。渐进式扩容会使用一个字段记录旧桶的位置,还有一个字段来记录迁移的进度(如下一个要迁移的旧桶编号),在每次的map存取时,会检测当前是否处于扩容阶段,若是,则进行一部分的键值对迁移,所以就将迁移时间平均了,避免了性能的瞬时抖动

匿名函数与闭包

函数值:函数值是一个指针,指向runtime.funcval类型的结构体,不直接指向函数地址是为了闭包的实现,其中保存着函数地址和闭包函数捕获的变量(不是闭包则为空)

闭包可简单地理解为有状态的函数,可见以下示例:

func adder() func(int) int {
  sum := 0
  return func(x int) int {
    sum += x
    return sum
  }
}

func main() {
  pos, neg := adder(), adder()
  for i := 0; i < 10; i++ {
    fmt.Println(pos(i), neg(-2*i))
  }
}

闭包:https://www.bilibili.com/video/BV1ma4y1e7R5

匿名函数:无名字的函数

闭包:有一个或多个在函数外部定义但在函数内部引用的自由变量。捕获了自由变量的匿名函数是闭包,一个闭包继承了函数声明时的作用域

匿名函数在作用域中捕获的变量和常量是引用传递,不是值传递,只有通过参数向匿名函数进行传递的变量才是值传递。所以在循环里创建的无参匿名函数中记录的是循环变量的内存地址,而不是循环变量某一时刻的值

//结果为 4 4 4 4
//defer最后执行时,循环已经结束,val此时为4,而匿名函数捕获的是引用,所以4次的printf都指向val的地址
l1 := []int{1,2,3,4}
for _, val := range l1 {
    defer func() {
        fmt.Printf("%v ", val)
    }()
}

//通过参数传递,结果为 4 3 2 1
l1 := []int{1,2,3,4}
for _, val := range l1 {
    defer func(val int) {
        fmt.Printf("%v ", val)
    }(val)
}

狭义的闭包

  1. 第一,外层函数嵌套内层函数
  2. 第二,内层函数使用外层函数的局部变量
  3. 第三,把内层函数作为外层函数的返回值!经过这样的三步就可以形成一个闭包

闭包就可以在全局函数里面操作另一个作用域的局部变量

goroutine

流水线goroutine要具备一些特质:它负责创建并关闭channel(在完成自己的工作后),这样外部调用无需关心channel的创建和关闭,当channel被关闭,它的下游goroutine会读出零值的数据。

g0 goroutine退出后,其他goroutine强制退出,程序结束

流水线:

  1. 一对一:一个channel发送到另一个channel
  2. 扇出:同一个 channel 可以被多个函数读取数据,直到channel关闭。这种机制允许将工作负载分发到一组worker,以便更好地并行使用 CPU 和 I/O。
  3. 扇入:多个 channel 的数据可以被同一个函数读取和处理,然后合并到一个 channel,直到所有 channel都关闭。

下游要具有通知上游goroutine退出的能力,即如果下游goroutine退出(正常或异常)而不通知上游,则上游会不断发送数据到channel直到阻塞,引起资源泄漏
流水线可通过两种方式解除发送者的阻塞:

  1. 提供足够大的缓冲保存发送者发送的数据(不方便,必须提前知道所要发送数据的数量)
  2. 接收者goroutine退出时,显式地通知发送者(可通过关闭一个channel来通知,如下)
  3. 使用context,详见context。相比于第2种方式,其优势在于可简单地通知多个goroutine
//上游
for n := range c {
    select {
    case out <- n:
    case <-done:
        return
    }
}
//下游
done := make(chan struct{})
defer close(done)

流水线
并发可视化

context

chan+select的方式,是一种比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。
上面说的这种场景是存在的,比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。

//通知一个goroutine

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go func(ctx context.Context) {
    for {
      select {
      case <-ctx.Done():
        fmt.Println("监控退出,停止了...")
        return
      default:      
        fmt.Println("goroutine监控中...")
        time.Sleep(2 * time.Second)
      }
    }
  }(ctx)

  time.Sleep(10 * time.Second)
  fmt.Println("可以了,通知监控停止")
  cancel()
  //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine
在goroutine中,使用select调用<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。
那么是如何发送结束指令的呢?这就是示例中的cancel函数啦,它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。

//通知多个goroutine

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go watch(ctx,"【监控1】")
  go watch(ctx,"【监控2】")
  go watch(ctx,"【监控3】")

  time.Sleep(10 * time.Second)
  fmt.Println("可以了,通知监控停止")
  cancel()
  //为了检测监控过是否停止,如果没有监控输出,就表示停止了
  time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
  for {
    select {
    case <-ctx.Done():
      fmt.Println(name,"监控退出,停止了...")
      return
    default:
      fmt.Println(name,"goroutine监控中...")
      time.Sleep(2 * time.Second)
    }
  }
}

示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
https://segmentfault.com/a/1190000006744213

defer

defer语句会在函数的return语句更新返回值之后执行(所以defer能够对命名返回值进行重写,返回预期之外的值),即使外部函数出现异常被中断,该延迟语句也将运行
经常用于处理成对的操作的资源释放,如关闭文件、关闭连接、释放锁等,因为其确保即便函数出错退出,资源还是能够被释放

还可以使用 defer 在任何函数开始后和结束前执行配对的代码,要使用此功能,需要创建一个函数并使它本身返回另一个函数,返回的函数将作为真正的延迟函数。在 defer 语句调用父函数后在其上添加额外的括号来延迟执行返回的子函数如下所示

func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the extra parentheses
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { 
        log.Printf("exit %s (%s)", msg,time.Since(start)) 
    }
}
// 输出
// enter bigSlowOperation
// exit bigSlowOperation (10.000589217s)

https://learnku.com/articles/37882

recover必须在defer函数中运行

recover捕获的是祖父级调用时的异常,直接调用时无效:

func main() {
    recover()
    panic(1)
}

直接defer调用也是无效:

func main() {
    defer recover()
    panic(1)
}

defer调用时多层嵌套依然无效:

func main() {
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

必须在defer函数中直接调用才有效:

func main() {
    defer func() {
        recover()
    }()
    panic(1)
}

defer实现

go1.12:每个goroutine在运行时都有一个对应的结构体runtime.g,其中一个字段指向链表头,链表结点是一个_defer结构体,保存在堆中,新注册的defer会添加都链表头,最后执行时也是从头开始,因为最后注册的defer先执行
go1.13:将defer结构体保存到当前栈帧的局部变量区,速度更快
go1.14:在编译将defer函数处理展开,在程序最后插入defer函数代码。但是这样有一个缺点,就是如果发生panic,则后边的代码执行不到,即defer函数执行不到,所以go1.14使用了栈展开技术,能够查寻到所有的defer函数,只不过性能会变慢。但程序发生panic的概率较低,所以整体性能还是有比较大的提升的

panic实现

runtime.g,其中一个字段指向panic链表头,当发生新的panic时,会插入到链表头,当打印panic信息时,会从链表尾开始打印,因为最先注册的panic是先发生的,所以应该先打印输出

方法

方法只能被声明到自定义类型(type关键字声明的类型,但有等号的声明如type myInt = int只是另外起了个别名),但是除了指针和interface,其中指针类型指的是如type pointType *int这种,而不是定义方法时传递的指针,如func (t *type1) method1(){}

不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。

方法调用形式:

type mStr struct {
  str string
}

func (m mStr) getStr() string {
  return m.str
}

s2 := mStr{"12"}
//1. 使用对象调用
s2.getStr()
//2. 使用类型的方法,但第一个参数必须传入一个对象,相当于this指针
mStr.getStr(s2)
//3. 通过函数值调用
fm := mStr.getStr
fm(s2)

接口实现

每一个类型都有一个类型元数据结构体,保存了类型的相关信息如类型名称、类型大小、对齐边界和是否是自定义类型等,如果是自定义类型,还会记录类型的包路径、方法的数目和方法元数据数组

空接口

两个字段:动态类型和动态值。当空接口被赋值后,动态类型指向所赋值类型的类型元数据,而动态值则指向所赋的值

非空接口

两个字段:itab结构体和动态值。其中itab结构体中保存动态类型、类型哈希值和此接口的类型元数据如要实现的方法列表等

反射

reflect用于获取类型元数据,因为类型元数据是未导出的,所以reflect包中又重新定义了一遍,包括空接口类型。reflect.TypeOf和reflect.ValueOf传递参数的都是接口类型,reflect拿到接口的地址,然后强转为自己重新定义的空接口类型,因为在reflect包中重新定义的和原本的一模一样,所以现在可以通过这个重新定义的接口来获取传入变量的类型元数据,相当于将原来未导出的类型元数据又暴露了出来

GPM模型

G-P-M模型是Go的调度器模型,对goroutine进行调度

goroutine优势:

  1. 上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;
  2. 内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;

G:goroutine、P:逻辑线程、M:系统线程

调度过程

当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。执行调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那划水啥都不干,它会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。

阻塞

  1. 用户态阻塞/唤醒
    当 goroutine 因为 channel 操作阻塞时,对应的 G 会被放置到某个 wait 队列(如 channel 的 waitq),该 G 的状态由_Gruning变为_Gwaitting ,而 M 会跳过该 G 尝试获取并执行下一个 G,如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为 runnable,尝试加入 G2 所在 P 的 runnext,然后再是 P 的 Local 队列和 Global 队列。

  2. 系统调用阻塞
    当 G 被阻塞在某个系统调用上时,此时 G 会阻塞在 _Gsyscall 状态,M 也处于 block on syscall 状态,此时的 M 可被抢占调度:执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它空闲的 M 绑定,继续执行其它 G。如果没有其它空闲的 M,但 P 的 Local 队列中仍然有 G 需要执行,则创建一个新的 M;当系统调用完成后,G 会重新尝试获取一个空闲的 P 进入它的 Local 队列恢复执行,如果没有空闲的 P,G 会被标记为 runnable 加入到 Global 队列。

参考链接:https://www.jianshu.com/p/105719434c29https://wudaijun.com/2018/01/go-scheduler/

GC垃圾回收

采用标记-清扫算法,支持主体并发增量式回收,使用插入与删除两种写屏障结合的混合写屏障

程序中用得到的数据一定是从栈、数据段这些根节点追踪得到的数据,追踪不到的则一定是垃圾,用“可达性”来近似代表”存活性“。使用“三色标记法”进行追踪标记

三色抽象法:一开始所有数据为白色,然后把能够直接追踪到的节点(即栈和数据段上的数据)标记为灰色,灰色表示基于当前节点的追踪还未完成,当基于某个节点的追踪任务完成后,便会将该节点标记为黑色,当所有节点都是黑色和白色时,追踪完成,释放白色节点的空间

增量式垃圾回收(对单核来讲):用户程序和垃圾回收程序交替进行,而不是垃圾回收一口气完成(STWstop the world)
主体并发式垃圾回收(对多核来讲):在某一阶段采用STW,其他阶段用户程序和垃圾回收程序并行进行

https://www.bilibili.com/video/BV1n5411H7qS

内存管理

Golang运行时的内存分配算法主要源自Google的TCMalloc算法,全称Thread-Caching Malloc。核心思想就是把内存分为多级管理,从而降低锁的粒度。
每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,有两个优点:

  1. 避免不同线程对全局内存池访问时的加锁延时
  2. 在用户态进行分配,无需进行系统调用

TCMalloc

内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存

  1. Page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。x64下Page大小是8KB。
  2. Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
  3. ThreadCache:每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
  4. CentralCache:是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
  5. PageHeap:PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。如下图,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。

go内存管理

/images/go/mem.jpg

但mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache,因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问,线程的运行又是与P绑定的,把mcache交给P刚刚好。

不同于TCMalloc,其只将对象分为大小两类:
/images/go/mem2.webp
小对象是在mcache中分配的,而大对象是直接从mheap分配的

看这篇文章就行:https://mp.weixin.qq.com/s/3gGbJaeuvx4klqcv34hmmw

time

time.Tick()相当于定时器,返回一个channel,每个一段时间产生一个tick
time.After()相当于一个计时器,返回一个channel,只产生一次tick
其中,相比于time.Tick(),更推荐使用time.NewTicker,因为tick是一个go routine,一旦开始则无法停止,当不再使用它是,会浪费资源。只有当在整个程序的运行周期内都需要tick的时候才考虑time.Tick(),否则使用time.NewTicker():

ticker := time.NewTicker(1*time.Second)
<-ticker.C
ticker.Stop() //停止定时的goroutine,释放资源

sycn

sycn.WaitGroup可检测go routine是否结束

var n sync.WaitGroup // in main
n.Add(1) //in main
    n.Done() //in go routine
n.Wait()  //in main

sync.Map

可对map进行并发存取,不必再使用原生map加锁实现

var m sync.Map
m.Store("key", 123)
v, ok := m.Load("key")
m.Delete("key")

https://wudaijun.com/2018/02/go-sync-map-implement/

竞争条件

不要使用共享数据来通信,而是使用通信来共享数据,即推荐使用channel而不是全局变量
使用channel传输数据不会发生竞争条件;只有对其他全局变量(如Int,map,slice等)进行读写才会发生,需要互斥技术

若同时读写一全局变量可使用锁:

  1. 互斥锁:sync.Mutex:Lock、Unlock
  2. 读写锁:sync.RWMutex:RLock、RUnlock、Lock、Unlock 适用于获得锁的go routine大多数是读操作,比互斥锁要慢一点
    1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁
    2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁
    3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁

一次性初始化可用sync.Once
/images/go/syncOnce.png

mutex传递给外部的时候需要传指针,不然就是实例的拷贝,会引起锁失败
其他sync.Mutex注意事项:https://blog.csdn.net/fwhezfwhez/article/details/82900498

判断竞争条件可使用go的竞争检查器,只要在go build或go run或go test命令后加上-race即可

  1. 竞争检查器会报告所有的已经发生的数据竞争
  2. 但是只能检查到运行时的竞争条件,所以在测试时,应尽量覆盖到你的包

函数调用栈

一个函数的栈被称为一个栈帧,栈基bp,栈顶sp

//示例程序
1 func main() {
2     a, b = 1, 2      
3     c := add(a, b)  
4     fmt.Println(c)   
5 }
6 
7 func add(a, b int) int {
8     c := a + b     
9     d := setVal(c)
10    return c     
11 }

当如上函数执行到第3行时,将依次发生如下动作:

  1. 压栈下一条指令的地址:即第4行地址,即call指令将ip压栈,使得函数返回后能够继续执行
  2. 压栈调用者函数的基址bp(即main函数的bp):因bp要指向被调函数(add)的栈基,所以要保存下来
  3. 调用add函数,开始设置add函数的栈帧
    1. 为add函数开辟足够的栈空间(编译时确定)
    2. 栈帧中自上而下依次是:add函数的局部变量(即c)、add函数中被调函数的返回值、add函数中被调函数的参数

参考:https://www.bilibili.com/video/BV1WZ4y1p7JThttps://www.bilibili.com/video/BV1tZ4y1p7Rv

go module

一个 module 是指一组相关的 package,通常对应于一个 git 仓库,module 是代码交换和版本管理的基本单位,即 module 的依赖也是一个 module

go mod init        //初始化 go module
go mod tidy        //自动添加缺失的包(并下载到本地)且移除不需要的包
go mod download    //下载 go.mod 中的依赖到本地 Cache ($GOPATH/pkg/mod下)
go mod edit -module newModuleName  //改变module名字

//推荐使用go的workspace
go mod edit -replace example.com/greetings=../greetings  //使用自己编写的本地包时,go从网络上找不到,此时可以手动指定本地的module目录路径

plugin

go build -buildmode=plugin file/pkg/dir

type Driver interface {
    Name() string
}

func main() {
    p, err := plugin.Open("driver.so")
    if err != nil {
     panic(err)
    }

    newDriverSymbol, err := p.Lookup("NewDriver")
    if err != nil {
        panic(err)
    }

    newDriverFunc := newDriverSymbol.(func() Driver)
    newDriver := newDriverFunc()
    fmt.Println(newDriver.Name())
}

编译和安装

go build  //编译生成可执行文件
go install  //安装到用户家目录下的go/bin/目录中

工作区workspace

在工作区中可以对多个module(含有go.mod的目录)进行代码编写,否则当同时打开多个module目录时会报错

假如目录层级为:
workspace---hello   
         |
         ---other1  
         |
         ---other2  

go work init // ,初始化工作区,在workspace目录下运行
go work use ./hello
go work use ./other1
go work use ./other2
go work use ./hello ./other1 ./other2 //或一次添加多个文件

泛型

func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
  var s V
  for _, v := range m {
      s += v
  }
  return s
}

SumIntsOrFloats(ints_map)  

SumIntsOrFloats[string,int64](ints_map)

定义类型参数
type Number interface {
    int64 | float64
}

故可以写成
func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {

}

单元测试

如要对hello.go文件中的函数进行测试,则创建已_test结尾的文件,如hello_test.go或myfunc_test.go

测试函数的名称必须以Test开头,如func TestHello(t *testing.T)

func TestReverse(t *testing.T) {
  testcases := []struct {
   in, want string
  }{
   {"Hello, world", "dlrow ,olleH"},
   {" ", " "},
   {"!12345", "54321!"},
  }

  for _, tc := range testcases {
   rev := Reverse(tc.in)
   if rev != tc.want {
     t.Errorf("Reverse: %q, want %q", rev, tc.want)
   }
  }
}
go test -v //测试文件中所有的测试函数
go test -v -run=TestReverse  //对指定测试函数进行测试

单元测试有局限性,即每个输入都必须由开发人员添加到测试中。模糊测试的一个好处是它为您的代码提供输入,并且可以识别您提出的测试用例未达到的边缘用例。

模糊测试 fuzzing

通过模糊测试可以发现一些漏洞如SQL注入、缓冲区溢出、拒绝服务和跨站点脚本攻击等

模糊测试可以与单元测试写在同一文件中

模糊测试的函数名以Fuzz开头,如func FuzzHello(f *testing.F)

模糊测试不需要像单元测试那样手动输入测试内容和结果进行比对,所以如何在模糊测试中保证测试的准确性呢?

模糊测试主要测试函数的特性,如Reverse函数是反转字符串,其中一个特性就是经过两次反转后与原字符串相同,所以可通过比较两次反转后的字符串与原字符串是否相同来进行测试;另一个特性是输入的字符串是utf-8编码的,那么经过反转后的字符串也应该保持相同的编码

func FuzzReverse(f *testing.F) {
  testcases := []string{"Hello, world", " ", "!12345"}
  for _, tc := range testcases {
   f.Add(tc) // Use f.Add to provide a seed corpus
  }
  
  f.Fuzz(func(t *testing.T, orig string) {
   rev := Reverse(orig)
   doubleRev := Reverse(rev)
   if orig != doubleRev {
     t.Errorf("Before: %q, after: %q", orig, doubleRev)
   }
   if utf8.ValidString(orig) && !utf8.ValidString(rev) {
     t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
   }
  })
}
go test -v -run=FuzzReverse //对指定测试函数进行种子输入测试
go test -v -fuzz=Fuzz //首先不指定Fuzz来确保种子输入通过,然后通过`-fuzz=Fuzz`来指定使用fuzzing进行模糊测试
go test -v -fuzz=Fuzz -fuzztime 30s //设置模糊测试时间

其他

  1. go没有重载