golang @any%
Publish on 2024-01-24

💡 Notion Tip: Please read golang修养之路 to develop a basic understanding about go. A lot of the content is from that book. I assume you have already known the syntax and memory model in c.

GO哲学圣经镇楼:
Do not communicate by sharing memory; instead, share memory by communicating.

nil

In Go, nil can represent zero values of the following kinds of types:

  • pointer types (including type-unsafe ones).
  • map types.
  • slice types.
  • function types.
  • channel types.
  • interface types.

Interface & Memory model

在 Go 语言中,接口类型的内存模型是指,一个接口变量实际上只是一个包含指向底层数据的指针和类型信息的数据结构。该指针指向实现了该接口的具体类型的实例。因此,使用接口类型时,实际上使用的是底层数据的指针。

type Animal interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d *Dog) Speak() string {
    return "Woof!"
}

我们现在将 Dog 类型的值赋给 Animal 类型的变量:

var animal Animal = &Dog{"Fido"}

在这个例子中,我们为 Dog 结构体实现了 Speak() 方法,并将其赋给了 Animal 接口类型的变量 animal。由于 Dog 实现了 Animal 接口,所以可以将 Dog 实例作为 Animal 类型的值传递。

在这里,变量 animal 中实际上存储了 Dog 实例的地址信息,以及对应的类型信息。当我们调用 animal.Speak() 方法时,由于 Dog 类型实现了 Speak() 方法,Go 会自动将底层值指针转换为 *Dog 类型,并调用 Speak() 方法。

需要注意的是,由于指针包含了地址信息,因此通过接口来操作变量的时候,会使用额外的内存进行指针的转换。此外,当使用接口类型时,由于需要动态判断底层数据的类型,因此会导致一些性能上的损失。编译器来检查一个类型是否实现了某个接口的所有方法。如果一个类型没有实现某个接口的全部方法,则会在编译时产生错误。

const

C++ 和 Go 两种语言中的常量有所不同,可能导致它们在寻址方面的特性表现不同。

在 C++ 中,常量(如 const int a = 10****)实际上是变量,只是它们被声明为不可修改(即 const 类型),因此编译器会将这些变量存储在内存中的只读区域。因为它们实际上是变量,在内存中占用了一定的空间,所以可以对其取地址。

而在 Go 中,常量(如 const a = 10)是无类型的,它们在编译时就被解析并直接替换为使用该常量的值。也就是说,在编译时,它们不会分配任何内存,并且不属于程序的运行时状态。因此,Go 编译器不支持获取常量的地址,因为它们没有在内存中分配任何空间。

总的来说,C++ 和 Go 中的常量有着不同的实现方式和语义约束。C++ 中的常量是实际的变量,但被限制为不可修改;而 Go 中的常量则是编译时常量,它们在程序运行时不存在

需要注意的是,某些 C++ 编译器可能会将常量优化为立即数,这样,它们就不会在内存中分配任何空间。这种优化行为类似于 Go 中的常量实现方式。因此,在 C++ 中也应该避免对常量进行不必要的取地址操作,这些操作可能会被编译器优化掉,或者在某些平台上导致未定义的行为。

make & new

make只能用于 slice, map 和 chan

Declaration of var

var i int
var s string

变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是 "",引用类型的零值是 nil

package main

import (
	"fmt"
)

func main() {
	var i *int
	*i = 10
	fmt.Println(*i)
}

以上代码会报错,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,正确代码如下

func main() {
  
   var i *int
   i=new(int)
   *i=10
   fmt.Println(*i)
  
}
  • new 函数用于分配一个指定类型的零值并返回该类型的指针。换句话说,new(T) 返回类型为 T 的指针,并将其初始化为 T 的零值。例如,new(int) 创建一个整数类型的指针,并将其初始化为零值 0
  • make 函数用于创建引用类型(如 slice、map 和 channel)的对象,并返回它们的引用。它接受一个类型和一些适当的参数,然后返回初始化后的对象。例如,make([]int, 10) 将创建一个初始长度为 10 的整数切片,并返回该切片的引用。

new always allocate memory on heap?

In Go, the new function always returns a pointer to a newly allocated zeroed value of the specified type. However, whether that memory is allocated on the heap or not depends on the context in which the new function is called. If it’s used with a composite literal, such as new(T) where T is a struct type, then the memory is allocated on the heap. This is because the returned pointer must survive beyond the lifetime of the function that called new.

However, if new is used with a built-in type or an array type, then the memory is allocated on the stack if possible. This is because these types have a fixed size and can be efficiently allocated on the stack.

Escape

go中,这段代码是不会出问题的 rnm,退钱!!!

package main

func foo(arg_val int)(*int) {

    var foo_val int = 11;
    return &foo_val;
}

func main() {

    main_val := foo(666)

    println(*main_val)
}

go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis)当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。

简而言之,不管你想把变量放到栈上还是堆上,编译器觉得应该放在哪,那它就得放在哪,一切以程序的运行逻辑为准。

Pointer type & struct type

package main

import "fmt"

type User struct {
    Name string
}

func (u *User) Greeting() string {
    u.Name = u.Name+" modify"
    return fmt.Sprintf("Greetings %s!", u.Name)
}

func main() {
    p := &User{"cppgohan by pointer"}
    u := User{"cppgohan by value"}

    fmt.Println(p.Greeting(), p)
    fmt.Println(u.Greeting(), u)
}

p.Greeting()u.Greeting()的调用方式本质上没有区别

对于 p,调用 Greeting() 就是 p.Greeting()。对于 u 隐式转换为调用 (&u).Greeting()

Garbage collection (IMPORTANT!)

Golang三色标记 混合写屏障GC模式全分析

mark and sweep algorithm

此算法主要有两个主要的步骤:

  • 标记(Mark phase)
  • 清除(Sweep phase)

Mark and Sweep算法在执行的时候,需要程序暂停,即 STW(Stop The World)。STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。

一些理解

想要理解gc,首先需要理解的是堆和栈的区别,栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用,所以我们希望的是尽可能地减少gc和栈的关联,或者说对栈的元素的gc应该速战速决。

$写屏障=插入屏障+删除屏障$

插入屏障为了解决这个问题,基于强三色不变式在开始和结束的时间对栈进行了两次扫描,第一次为并发扫描,第二次为STW

基于弱三色不变式的删除屏障是比较难理解的(删除屏障中就已经开启了插入屏障,来应对堆区黑色对象引用新建的孤独对象的情况),这里写一下我的理解。设父对象为x,其引用对象为y,最基础的删除屏障,是将删除的引用全部变为灰色(除非已经是黑色),对栈和堆的对象都是有效的。分两种情况讨论:

  1. 父对象本来就不可达:此时,反而是救活了这个本应该被删除的对象,但是对不可达对象的操作本来就是违规的(涉及野指针),可以不考虑。
  2. 父对象可达:可以等价认为可达y的链路上存在灰色对象,此时,为了让其他的黑色对象(特指栈上的黑色对象,因为栈上的对象不开启写屏障)可以自由引用yy链路上的子对象(在删除屏障之前,不然又是野指针了),需要将y无脑变灰来满足弱三色不等式。

可以理解到,如果yy链路上的子对象没有被黑色对象引用,那么删除屏障实际上是活佛再世,抬了这些入土半截的对象一手,回收精度是偏低的。

Untitled.png

那么混合写屏障呢?

  1. 前面说到的插入屏障明显针对的是堆区的元素为黑色的情况,加之中途还可能有新创建的栈对象,所以最后还要使用STW进行栈对象的清理。删除屏障也是没有考虑中途加入的栈元素。于是,可以优化的是:

栈上新加入的对象直接变黑

  1. 对于删除屏障的栈对象:

你一个一个从白变灰变黑,回收精度还没提高多少,不如一步到位,在开始的时候就并发扫描直接变黑

  1. 既然栈对象直接就黑了:

堆区的对象可能被栈区的黑色对象引用,删除屏障的祖宗之法不可变

  1. 连带的:

堆区的插入屏障作为删除屏障的影子,也就跟着了。

写屏障(删除屏障+插入屏障),其根本目的就是为了保护对象不被删除。而混合写屏障则是结合了go并发的优势,尽可能减少STW的gc方法。

ATTENTION: 混合写屏障并不能完全消除STW,尽管从原理来看,混合写屏障已经是 STW free 的了,但是由于go的高并发特性,为了使gc的结果保持一致性,在某些时候还是需要 STW 的。

Complement

map

map

为什么Go语言中的map并不支持直接对map元素进行取址操作

由于 map 中的元素存储在哈希表中,并且哈希表可能会因为容量变化而重新分配内存,因此对 map 元素进行取址操作并不可行。如果你试图对 map 元素进行取址操作,编译器会报错,提示可能导致指针无效。简而言之,对map元素的修改可能会导致哈希表rehash,所以不能直接修改。在并发程序中,可以使用互斥锁来保证在并发程序中对 map 进行安全的访问和修改。另外,还可以考虑使用 Go 语言提供的并发安全的映射类型 sync.Map,它可以安全地在多个 goroutine 中读取和写入数据。

以下代码有什么问题,说明原因

package main

import (
    "fmt"
)

type student struct {
    Name string
    Age  int
}

func main() {
    //定义map
    m := make(map[string]*student)

    //定义student数组
    stus := []student{
        {Name: "zhou", Age: 24},
        {Name: "li", Age: 23},
        {Name: "wang", Age: 22},
    }

    //将数组依次添加到map中
    for _, stu := range stus {
        m[stu.Name] = &stu
    }

    //打印map
    for k,v := range m {
        fmt.Println(k ,"=>", v.Name)
    }
}

遍历结果出现错误,输出结果为

zhou => wang
li => wang
wang => wang

map中的3个key均指向数组中最后一个结构体。

0xc000010030
0xc000010030
0xc000010030
zhou => wang
li => wang
wang => wang

从内存的角度来看,3个key都对应 &stu这个指针,这个指针的内存地址是临时的,同时也可以推断出在golang中,for循环的临时变量的虚拟地址是复用的。所以结果出错。

正确写法

// 遍历结构体数组,依次赋值给map
for i := 0; i < len(stus); i++  {
    m[stus[i].Name] = &stus[i]
}

slice

slice

compile

compile

context

context

reflect

reflect

unsafe

unsafe

interface

interface

channel

channel

GMP

GMP

Golang的协程调度器原理及GMP设计思想

对于操作系统而言进程、线程以及Goroutine协程的区别

控制goroutines数量

Go是否可以无限go?如何限定数量?

  • 方法一:只是用有buffer的channel来限制 (无法等待回收goroutine结束,资源回收)
  • 方法二:只使用sync同步机制 (无法控制数量)
  • 方法三:channel与sync同步组合方式 (perfect)
  • 方法四:利用无缓冲channel与任务发送/执行分离方式

WaitGroup与goroutine的竞速问题

WaitGroup

总结:在goroutine前add WaitGroup会好一些。

© 2024 humbornjo :: based on 
nobloger  ::  rss