GO语言圣经简略

216 阅读2分钟
  • ps: 本书时间有点老(15年),读的时候需慎重
  • 本文章只是对这本书的读后感,感兴趣请购买正版
  • 阅读本文之前,希望你已经基本了解了go的基本数据类型以及流程控制

一. 入门

  • 1.1 变量声明

s := ""            // 只作用于当前作用域,即{}内
var s string       // 依赖string类型的内部初始化机制,初始化为空字符串
var s = ""         // 很少使用
var s string = ""  // 显示标明变量类型
// 通常用到第一种第二种为主
package main

import "fmt"

func main() {
	var a = 1
	b := 2
	{
		b = 3
		a = 4
		fmt.Println(a)
		fmt.Println(b)
		// 3,4
	}
	func() {
		a = 5
		b = 6
		fmt.Println(a)
		fmt.Println(b)
		// 5,6
	}()
	fmt.Println(a)
	fmt.Println(b)
	// 5,6
}
  • 1.2 map迭代

map_exm = make(map[string]int)
map_exm["test"] = 1
map_exm["test_ii"] = 2
for k,v := range map_exm {
    fmt.Println(k,v)
}

对map进行range循环的时候,其迭代顺序是不确定的,从实践上看,可能每次运行结果都不一样

  • 1.3 fmt.printf(),fmt.sprintf()

%d int
%x, %o, %b 16进制,8进制,2进制度
%f, %g, %e 浮点数 e是科学计数法
%t 布尔变量
%c rune,unicode
%s string
%q 带双引号string或者单引号的rune
%v 变量
%T 类型

  • 1.4 简单的go demo


// 获取单个
func TestGetUrlBody(){
    url := "http://yourtest.com"
    resp,err := http.Get(url)
    if err != nil {
        // os.Stderr 输出在屏幕
        // os.Stdout 输出在文件
        fmt.Fprintf(os.Stderr, "%v", err)
        // 让当前程序退出,并且不会执行defer函数
        os.Exit(1)
    }
    b,err := ioutil.ReadAll(resp.Body)
    resp.Body.Close()
    if err != nil {
        fmt.Fprintf(os.Stderr, "%s:%v", url, err)
        os.Exit(1)
    }
    fmt.Printf("%s", b)
}

// goroutine 同时获取多个

func TestGetUrlBody(t *testing.T) {
	url := "http://www.baidu.com"
	resp, err := http.Get(url)
	if err != nil {
		// os.Stderr 输出在屏幕
		// os.Stdout 输出在文件
		fmt.Fprintf(os.Stderr, "%v", err)
		t.Logf("%v", err)
		// 让当前程序退出,并且不会执行defer函数
		os.Exit(1)
	}
	b, err := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s:%v", url, err)
		os.Exit(1)
	}
	fmt.Printf("%s", b)
}

func TestGetUrlBodyAsyncs(t *testing.T) {
	url := "http://www.baidu.com"
	ch := make(chan string, 10)
	for i := 0; i < 2; i++ {
		go asyncHandle(url, ch)
	}
	for temp := range ch {
		fmt.Println(temp)
	}
}

func asyncHandle(url string, ch chan string) {
	resp, err := http.Get(url)
	if err != nil {
		// os.Stderr 输出在屏幕
		// os.Stdout 输出在文件
		ch <- fmt.Sprintf("%v", err)
		// 让当前程序退出,并且不会执行defer函数
		os.Exit(1)
	}
	b, err := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		ch <- fmt.Sprintf("%v", err)
		os.Exit(1)
	}
	ch <- string(b)
}



二. 程序结构

  • 1.指针

指针的值是变量的内存空间地址。

func f() *int {
    v := 1
    return &v
}
var p = f()

fmt.Println(p == f());

/*
这段代码的输出结果是 `false`。

在这段代码中,`f()` 函数返回一个指向局部变量 `v` 的指针。
当我们在 `main` 函数中调用 `f()` 时,
它会返回一个指向新的 `v` 变量的指针。
因此,`p` 和 `f()` 返回的指针是不同的,它们指向不同的变量。
所以 `p == f()` 的结果是 `false`*/

  • 2.new函数

p := new(int)
*p = 2
fmt.Println(*p)

/**
这段代码的输出结果是 `2`。

在这段代码中,我们使用 `new` 函数创建了一个新的 `int` 类型的指针 `p`,
并将其初始化为 `nil`。
然后,我们将 `p` 指向的内存地址上的值设置为 `2`,
并使用 `*p` 访问该值。
最后,我们使用 `fmt.Println` 函数将该值打印到控制台上。
 */
 
 p := new(int)
 q := new(int)
 fmt.Println(p == q) // false
 fmt.Println(*p == *q) // true
 
 /**
 在这段代码中,我们使用 `new` 函数创建了两个新的 `int` 类型的指针 `p` 和 `q`,
 并将它们初始化为 `nil`。
 然后,我们使用 `==` 运算符比较 `p` 和 `q`,
 由于它们指向不同的内存地址,所以比较结果为 `false`。
 接着,我们使用 `*` 运算符访问 `p` 和 `q` 指向的内存地址上的值,
 由于它们都是新创建的 `int` 类型变量,所以它们的值相等,比较结果为 `true`。
  */
 
 
  • 3.变量的生命周期与作用域

在 Go 中,变量的生命周期由其作用域和存储位置决定。当一个变量被声明时,它会被分配到内存中的某个位置,并在其作用域内可见。当变量超出其作用域时,它将被销毁并释放其占用的内存。 在GO中,变量的作用域是指该变量名的有效范围

以下是 Go 变量的生命周期的一些常见情况:

  1. 局部变量:当一个局部变量被声明时,它会被分配到栈上,并在其作用域内可见。当函数返回时,该变量将被销毁并释放其占用的内存。
  2. 全局变量:全局变量在程序启动时被分配到内存中的静态存储区,并在整个程序的执行期间都可见。当程序结束时,该变量将被销毁并释放其占用的内存。
  3. 堆上分配的变量:当使用 new 或 make 函数创建一个变量时,它将被分配到堆上,并在整个程序的执行期间都可见。当该变量不再被引用时,它将被垃圾回收器回收并释放其占用的内存。
  4. 闭包变量:当一个变量被捕获到闭包中时,它将被分配到堆上,并在整个闭包的执行期间都可见。当闭包不再被引用时,它将被垃圾回收器回收并释放其占用的内存。

总之,Go 变量的生命周期由其作用域和存储位置决定。在函数内部声明的变量通常在函数返回时被销毁,而在堆上分配的变量通常在不再被引用时被垃圾回收器回收。

以下是go变量作用域的一些常见情况: 在 Go 中,变量的作用域由它们声明的位置决定。一般来说,变量的作用域可以分为以下几种情况:

  1. 函数内部的变量只在该函数内部可见,称为局部变量。
  2. 函数外部的变量在整个包内可见,称为全局变量。
  3. 函数参数的作用域只在该函数内部可见,称为形式参数。
package main

import "fmt"

var globalVar = "global"

func main() {
    localVar := "local"
    fmt.Println(localVar)
    fmt.Println(globalVar)
    printVar()
}

func printVar() {
    fmt.Println(globalVar)
}

/**
打印的结果为:
local
global
global
 */
  • 4.赋值

v, ok = m[key]
// 对于非接口类型的变量,不能使用类型断言。
v, ok = x.(T)
v, ok = <- ch

v, ok = m[key] 用于从 map 中获取指定 key 对应的 value 值,如果 key 存在,则将其对应的 value 值赋值给 v,并将 ok 设为 true;如果 key 不存在,则将 v 设为该类型的零值,将 ok 设为 false

v, ok = x.(T) 用于将接口类型 x 转换为类型 T,如果转换成功,则将转换后的值赋值给 v,并将 ok 设为 true;如果转换失败,则将 v 设为该类型的零值,将 ok 设为 false

v, ok = <- ch 用于从通道 ch 中接收一个值,如果通道没有关闭且有值可接收,则将接收到的值赋值给 v,并将 ok 设为 true;如果通道已关闭或没有值可接收,则将 v 设为该类型的零值,将 ok 设为 false

  • 5.类型转换T

在 Go 中,类型转换可以使用 T() 的形式进行。其中 T 表示要转换的目标类型,可以是内置类型、自定义类型或接口类型。

如果要将一个值 x 转换为类型 T,则可以使用 T(x) 的形式进行转换。如果 x 的类型和 T 的类型不兼容,则会在编译时产生错误。

下面是一个示例,将一个 int 类型的值转换为 float64 类型:

package main

import "fmt"

func main() {
    x := 42
    y := float64(x)
    fmt.Printf("x = %d, y = %f\n", x, y)
}

在 Go 中,类型转换 T() 有一些需要注意的事项:

  1. 只能在兼容的类型之间进行转换。例如,不能将一个字符串转换为整数类型,因为它们的类型不兼容。
  2. 转换后的值可能会发生截断或精度丢失。例如,将一个浮点数转换为整数类型时,小数部分将被截断。
  3. 对于指针类型,可以将一个指向某个类型的指针转换为指向另一个类型的指针。但是,需要注意的是,这种转换可能会导致指针指向的内存地址发生变化。
  4. 对于接口类型,可以将一个实现了某个接口的类型转换为该接口类型。但是,需要注意的是,这种转换可能会导致接口值的动态类型和动态值发生变化。

三. 基础数据类型

1.rune,byte,unitptr

package main

import "fmt"

func main() {
    // rune 类型表示一个 Unicode 码点
    var r rune = '世'
    // %c用来表示一个字符,如果表示字符串,使用%s
    fmt.Printf("rune: %c, type: %T\n", r, r)

    // byte 类型表示一个字节
    var b byte = 'A'
    fmt.Printf("byte: %c, type: %T\n", b, b)

    // uintptr 类型表示一个指针的整数值
    var x int = 42
    var p uintptr = uintptr(unsafe.Pointer(&x))
    fmt.Printf("uintptr: %d, type: %T\n", p, p)
}

/**
输出结果为:
rune: 世, type: int32
byte: A, type: uint8
uintptr: 824634956288, type: uintptr
 */

在这个示例中,我们可以看到:

  1. rune 类型可以用来表示 Unicode 码点,它的实际类型是 int32
  2. byte 类型可以用来表示一个字节,它的实际类型是 uint8
  3. uintptr 类型可以用来表示一个指针的整数值,它的实际类型是一个无符号整数类型,大小和指针的大小相同。

需要注意的是,uintptr 类型通常用于底层编程,不建议在普通的应用程序中使用。

在Go中,rune和byte都是基本数据类型,但它们有着不同的用途和特点。

rune跟byte的详细区别:

rune是一个32位的Unicode字符,它可以表示任何Unicode码点。在Go中,rune通常用于处理文本和字符串,因为它可以表示任何语言的字符。例如,可以使用rune类型来遍历字符串中的每个字符,或者将字符串转换为rune切片以进行更高级的文本处理。

byte是一个8位的无符号整数,它通常用于处理二进制数据或字节流。在Go中,byte类型通常用于读取和写入文件、网络连接或其他数据源中的字节流。例如,可以使用byte类型来读取文件中的字节,或者将字符串转换为byte切片以进行网络传输。

请注意,rune和byte类型之间可以进行转换,但是这通常需要进行显式的类型转换。在处理文本和字符串时,应该使用rune类型,而在处理二进制数据和字节流时,应该使用byte类型。

2.算数运算符的抛出问题

package main

import "fmt"

func main() {
    var x uint8 = 255
    x = x + 1
    fmt.Println(x)
}

// 结果为0

package main

import "fmt"

func main() {
    var x int32 = 2147483647
    x = x * 2
    fmt.Println(x)
}

// 结果为-2

3.整型以及浮点数打印的问题

package main

import "fmt"

func main() {
    x := 12345
    fmt.Printf("%05d\n", x) // 输出:12345
    fmt.Printf("%010d\n", x) // 输出:0000012345
    
    x := 3.141592653589793
    fmt.Printf("%.2f\n", x) // 输出:3.14
    fmt.Printf("%.5f\n", x) // 输出:3.14159
    
    
    // 定义自定义位数的浮点数
    
    // 例如定义99位
     x := new(big.Float).SetPrec(99).SetFloat64(3.141592653589793)
    fmt.Println(x)
}

4 常量

const Pi64 float32 = math.Pi
var x float64 = float64(Pi64)
var z complex128 = complex128(Pi64)
fmt.Println(x)
// 3.141592653589793 
fmt.Println(z)
// 3.141592653589793+0i 
fmt.Println(Pi64)
// 3.1415927

我们可以明显看到,常量可以是无类型的,当常量被赋值给一个变量的时候,无类型的常量将 会被隐性的转换为对应的类型。

下面是常量的一些注意事项:

  1. 常量必须在声明时进行初始化。
  2. 常量的值必须是编译时可确定的,例如数字、字符串或布尔值。
  3. 常量可以是任何基本类型,如整数、浮点数、字符串和布尔值。
  4. 常量的名称应该使用大写字母,以便于区分变量和常量。
  5. 常量可以在函数内部声明,但是只能在函数内部使用。
  6. 常量可以用于枚举类型,例如定义一组相关的常量。

四 复合数据类型

1 数组,slice

数组是定长的

// 一个简单的例子
func TestArr(t *testing.T) {
	var a = [...]int{1, 2, 3, 4, 5, 6, 7}

	b := a[3:6]
	c := a[2:5]

	a[3] = 23
	// 在go run test的时候打印b和c的值
	t.Errorf("b should be [4 5 23], but got %v", b)
	// 23 5 6
	t.Errorf("c should be [3 4 23], but got %v", c)
	// 3 23 5
	t.Log("All tests passed!")
}

2.map

  • map,slice的比较
// (1)手动比较
func equalMaps(a, b map[string]int) bool {
    if len(a) != len(b) {
        return false
    }
    for k, v := range a {
        if b[k] != v {
            return false
        }
    }
    return true
}

// (2)使用deepequal

func equalStructs(a, b MyStruct) bool {
    return reflect.DeepEqual(a, b)
}

func equalStructs(a, b map[string]int) bool {
    return reflect.DeepEqual(a, b)
}

需要注意的是,使用`reflect.DeepEqual`函数比较map时,
它会比较map的键和值的顺序。
因此,如果两个map的键和值相同但顺序不同,
`DeepEqual`函数也会返回`false`// (3)可以使用cmp.Equal比较

func TestEqual(t *testing.T) {
    a := map[string]int{"foo": 1, "bar": 2}
    b := map[string]int{"bar": 2, "foo": 1}

    if !cmp.Equal(a, b) {
        t.Errorf("a and b should be equal, but they are not")
    }
    
    c := Person{Name: "Alice", Age: 30}
    d := Person{Name: "Alice", Age: 30}

    if !cmp.Equal(c, d) {
        fmt.Println("c and d are not equal")
    }
    
    
    e := []int{1, 2, 3}
    f := []int{1, 2, 3}

    if !cmp.Equal(e, f) {
        fmt.Println("e and f are not equal")
    }
}


  • map的元素,并不是一个变量,我们无法对map的元素进行取址操作
  • map的有序遍历
package main

import (
    "fmt"
    "sort"
)

func main() {
    // 定义一个map
    m := make(map[string]int)

    // 填充元素
    m["apple"] = 2
    m["banana"] = 3
    m["orange"] = 1

    // 获取map的key列表
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }

    // 对key列表进行排序
    sort.Strings(keys)

    // 遍历有序的key列表,并输出对应的value
    for _, k := range keys {
        fmt.Printf("%s: %d\n", k, m[k])
    }
}
  • 判断某个元素是否存在 test,ok := tests["one"]

  • 思考一:

var m = make(map[int]int,10)
m1 := m[1]
m2,ok := m[2]
fmt.Println(m1,m2,ok)
// 0 0 false
  • 思考二 map扩容之后delete会缩容吗

map的元素数量减少时,Go 会自动缩容 map,以便释放未使用的内存。因此,不需要手动触发 map 的缩容。 map 的缩容是在运行时进行的,并且不保证立即发生。当 map 中的元素数量减少时,Go 可能会等待一段时间,直到有足够的空闲空间来缩小 map 的容量。

  • 思考三 一些map内置的函数

image.png

3.struct与json

  • 字段名首字母大写:在Go语言中,如果一个struct的字段名首字母大写,那么它就是可导出的(exported),可以被其他包访问。如果字段名首字母小写,则它是不可导出的(unexported),只能在当前包内访问
  • 进行==|!=的时候,字段类型必须是可比较的:在Go语言中,struct的字段类型必须是可比较的,这意味着它们必须支持==和!=操作符。例如,slice、map和function类型都不是可比较的,因此不能直接用作struct的字段类型。
// 可比较的类型
type Person struct {
    Name string
    Age  int
}

type Book struct {
    Title  string
    Author string
}

type Point struct {
    X int
    Y int
}

type Date struct {
    Year  int
    Month int
    Day   int
}

// 不可比较的类型
type Employee struct {
    Name string
    Age  int
    Salary []int // slice类型不可比较
}

// 例如下面
type Employee struct {
    Name string
    Age  int
    []int // 就会出现编译错误
}

type Car struct {
    Brand string
    Model string
    Engine func() // function类型不可比较
}

type Location struct {
    Latitude  float64
    Longitude float64
    Altitude  float64
    Time      time.Time // time.Time类型不可比较
}
  • 嵌入其他struct:在Go语言中,可以通过嵌入其他struct来实现继承。嵌入的struct可以访问其字段和方法,就像它们是当前struct的字段一样。嵌入的struct可以是指针类型或非指针类型。
  • 匿名字段:在Go语言中,可以使用匿名字段来简化struct的定义。匿名字段是指没有字段名的字段,只有字段类型。在访问匿名字段时,可以直接使用字段类型作为字段名
// 示例1:嵌入非指针类型的struct
type Person struct {
    Name string
    Age  int
}

type Employee struct {
    Person
    Salary float64
}

func main() {
    emp := Employee{
        Person: Person{
            Name: "Alice",
            Age:  30,
        },
        Salary: 5000.0,
    }
    fmt.Println(emp.Name) // 直接访问嵌入的Person struct的Name字段
    fmt.Println(emp.Age)  // 直接访问嵌入的Person struct的Age字段
    fmt.Println(emp.Salary)
}

// 示例2:嵌入指针类型的struct
type Person struct {
    Name string
    Age  int
}

type Employee struct {
    *Person
    Salary float64
}

func main() {
    p := &Person{
        Name: "Alice",
        Age:  30,
    }
    emp := Employee{
        Person: p,
        Salary: 5000.0,
    }
    fmt.Println(emp.Name) // 直接访问嵌入的Person struct的Name字段
    fmt.Println(emp.Age)  // 直接访问嵌入的Person struct的Age字段
    fmt.Println(emp.Salary)
}

// 示例3:嵌入多个struct
type Address struct {
    Province string
    City     string
}

type Person struct {
    Name    string
    Age     int
    Address // 匿名字段
}

type Employee struct {
    *Person
    Salary float64
}

func main() {
    p := &Person{
        Name: "Alice",
        Age:  30,
        Address: Address{
            Province: "Guangdong",
            City:     "Shenzhen",
        },
    }
    emp := Employee{
        Person: p,
        Salary: 5000.0,
    }
    fmt.Println(emp.Name)      // 直接访问嵌入的Person struct的Name字段
    fmt.Println(emp.Age)       // 直接访问嵌入的Person struct的Age字段
    fmt.Println(emp.Province)  // 直接访问嵌入的Address struct的Province字段
    fmt.Println(emp.City)      // 直接访问嵌入的Address struct的City字段
    fmt.Println(emp.Salary)
}

4.context

func main() {
	func0()
}

func func0() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func1(ctx)
	go func2(ctx)
	time.Sleep(1 * time.Second)
}

func func1(ctx context.Context) {
	fmt.Println("这里是func1")
}

func func2(ctx context.Context) {
	fmt.Println("这里是func2")
	go func3(ctx)
}

func func3(ctx context.Context) {
	fmt.Println("这里是func3")
}
// 这里是func2
// 这里是func1
// 这里是func3

image.png

五 函数与方法

1.函数

  • 函数的形参与又名返回值作为函数最外层的局部变量,被存储在相同的语法块中

2.多返回值

  • 如果一个函数将所有的返回值都命名为变量名,那么该函数直接可以return 进行返回,我们称之为bare return
  • 如果有多处返回的时候,容易丢失原有语境,因此本书并不建议过度使用bare return

3.错误

  • 包错误只向上返回,最终在main中进行panic
  • 错误传递途中,如果需要对错误进行处理,尽量使用fmt.Errorf()

4.匿名函数

  • 在go中可以使用匿名函数(anonymous function)来定义没有名字的函数。匿名函数可以直接赋值给变量、作为参数传递给其他函数、或者在其他函数内部定义和调用。
  • 在匿名函数中,变量的生命周期并不由它的作用域决定。变量的生命周期取决于该匿名函数什么时候在栈中被销毁。
// 示例1:将匿名函数赋值给变量
add := func(a, b int) int {
    return a + b
}
sum := add(1, 2)
fmt.Println(sum) // 输出3

// 示例2:将匿名函数作为参数传递给其他函数
func apply(f func(int) int, x int) int {
    return f(x)
}
square := func(x int) int {
    return x * x
}
result := apply(square, 3)
fmt.Println(result) // 输出9

// 示例3:在其他函数内部定义和调用匿名函数
func main() {
    x := 1
    func() {
        x++
        fmt.Println(x) // 输出2
    }()
    fmt.Println(x) // 输出1
}

六 go的内存管理

1.内存分配

  • 内存分配 自动allocator(分配),自动collector(回收)
    • 内存分配的三个角色

      Mutator:指程序中的执行部分,也就是分配和使用内存的代码。Mutator 会在堆上分配内存,创建对象,并在对象之间建立引用关系。

      Allocator:指垃圾回收器中的分配器,负责为 Mutator 分配内存。Allocator 会维护一个空闲内存池,当 Mutator 请求内存时,Allocator 会从空闲内存池中分配一块内存,并返回给 Mutator。

      Collector:指垃圾回收器中的收集器,负责回收不再使用的内存。Collector 会定期扫描堆上的对象,标记出所有仍然被引用的对象,然后回收所有未被标记的对象。

image.png

  • 进程虚拟内存分布
    • 举例32位linux内存分布

image.png

2.Allocator基础

  • 基本的分配器类型
    • Bump Allocator 线性分配器
    • Free List Allocator 空闲链表分配器(演示地址)
      • First-Fit
      • Next-Fit
      • Best-Fit
      • Segregated-Fit(分级匹配算法,go就是这种算法的改进版本)

3.Malloc基础

Malloc实际上也是first-fit的算法的一种实现

brk是调整其堆顶的位置从而调整其虚拟内存的大小 mmap是从虚拟堆的任意一个位置调整其虚拟内存大小

image.png

image.png

  • 引申,malloc的应用实践

可以了解一下c的tcmalloc,jemalloc

TCMalloc 是由 Google 开发的内存分配器,旨在提高多线程应用程序的性能。它使用了一些高级技术,如线程本地缓存、中心缓存和分级分配器,以减少锁竞争和内存碎片。TCMalloc 还提供了一些工具,如堆分析器和内存泄漏检测器,以帮助开发人员诊断和解决内存问题。

jemalloc 是由 Facebook 开发的内存分配器,旨在提高多线程应用程序的性能和可扩展性。它使用了一些高级技术,如分配器缓存、分配器锁和分配器事件,以减少锁竞争和内存碎片。jemalloc 还提供了一些工具,如堆分析器和内存泄漏检测器,以帮助开发人员诊断和解决内存问题。

需要注意的是,TCMalloc 和 jemalloc 都是第三方库,需要在应用程序中显式地链接和使用。在使用这些库时,需要仔细阅读它们的文档,并根据应用程序的需求进行配置和调整。

  • 思考,为什么现在不再人工控制Allocator以及Malloc

[示例](▶ dangling pointer - memory management && garbage collection (figma.com))

4.go内存逃逸分析

推荐cms/compile/internal/gc/escape.go的逃逸分析源码以及 github.com/golang/go/tree/master/test中关于escape的测试用例

5.Go语言内存分配

  • 连续堆->稀疏堆

image.png

image.png

[示例]分配稀疏堆模拟

七 .垃圾收集

1.常用的几种垃圾回收算法

垃圾回收图示Visualizing Garbage Collection Algorithms (atomicobject.com)

  • 最后清除算法

NO_GC.gif

  • 引用计数算法

REF_COUNT_GC.gif

  • 标记清扫算法

MARK_SWEEP_GC.gif

  • 标记压缩算法

MARK_COMPACT_GC.gif

  • 复制算法清除

COPY_GC.gif

2.go语言垃圾回收使用的标记清扫算法mark_sweep

  • runtime.gc
  • runtime.mallocgc(内存增长率过高,会在内存分配的时候被动触发)
  • forcegchelper(定时gc,大概预估时间是2min) image.png

3.go语言垃圾回收的标记过程

  • gc标记的流程

  • 思考:假如在标记的过程中,该标记的对象被应用程序修改应该怎么办 使用