首页 > 技术文章 > go 学习笔记

ling11 2022-01-22 14:49 原文

go内存分配逃逸分析

https://www.iphpt.com/detail/137

逃逸分析(Escape Analysis):指由编译器解决内存分配的位置,不需要程序员指定。

在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束可自动将内存回收。
  • 如果分配在堆中,则函数执行结束可交给GC处理。

注意:对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力。

go build -gcflags=-m

逃逸分析的作用是什么?

  • 逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  • 逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配,而没有发生逃逸的则有编译器在栈上分配)
  • 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码会去掉同步锁运行。

逃逸总结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配的内存不需要GC处理
  • 堆上分配的内存使用完毕会交给gc处理
  • 逃逸分析摸底是决定内存分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

函数传递指针真的比传值效率高吗?

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能增加gc的负担,所以传递指针不一定是高效的。

TDD、BDD、ATDD、DDD 软件开发模式

https://blog.csdn.net/ejinxian/article/details/70212208

Go垃圾回收机制

http://interview.wzcu.com/Golang/Go垃圾回收机制.html

1 定义

垃圾回收gc:是在后台运行一个守护线程,它的作用是在监控各个对象的状态,识别并且丢弃不再使用的对象来释放和重用资源。

简单来说,垃圾回收的核心就是标记出那些内存还在使用中(即被引用到),那些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。

2 垃圾回收触发时机

内存分配量到阀值触发GC

每次内存分配时都会检查当前内存分配量是否已到达阀值,如果达到阀值则立即启动gc。

阀值=上次GC内存分配量*内存增长率

内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。

定时触发GC

默认情况下,最长2分钟触发一次GC,src/runtime/proc.go:forcegcperiod变量中被声明:

// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9

手动触发

程序代码中也可使用runtime.GC() 来手动触发GC,这主要用于GC性能测试和统计。

3 垃圾回收算法

引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁是,引用计数减1,当引用计数器为0时回收该对象。

  • 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
  • 缺点:不能很好地处理循环引用,而实时维护引用计数,也有一定的代价。
  • 代表语言:python、php

标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收。

  • 优点:解决了引用计数的缺点。
  • 缺点:需要STW,即要暂时停掉程序运行。
  • 代表语言:golang(三色标记法 白灰黑)

分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代又不通的回收算法和回收频率。

  • 优点:回收性能好
  • 缺点:算法复杂
  • 代表语言:java

golang中的垃圾回收主要应用三色标记法,gc过程和其他用户goroutine可并发运行,但需要一定时间的STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,golang进行了多次迭代优化来解决这个问题。

go单元测试

https://mp.weixin.qq.com/s/hqsBWrky2PtHxRPbzvWsNA

方式1:

package main

//拼接两个字符串
func JointString(a string, b string) string {
	return a + b
}

package main

import (
	"fmt"
	"testing"
)
//入门
func TestJointString(t *testing.T) {
	fmt.Println(JointString("a", "b"))
}
//初级
func TestJointString1(t *testing.T) {
	if JointString("a", "b") != "ab" {
		t.Error("与我们预判的结果不一致")
	}
	// 故意写一个错误的预判
	if JointString("g", "b") != "gob" {
		t.Error("与我们预判的结果不一致")
	}
}
//进阶
func TestJointString2(t *testing.T) {
	type args struct {
		a string
		b string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// 这里编写我们的预判逻辑
		{"testA", args{"d", "v"}, "dv"},
		{"testB", args{"c", "d"}, "cd"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := JointString(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("JointString() = %v, want %v", got, tt.want)
			}
		})
	}
}

你也可以在命令行里面运行,cd 到我们的 utils 目录下面,执行 go test 即可:

Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestJointString$ neuron/function/ptz/astr

=== RUN   TestJointString
ab
--- PASS: TestJointString (0.00s)
PASS
ok      neuron/function/ptz/astr        0.017s


> 测试运行完成时间: 2022/2/16下午1:50:16 <

-v 是让控制台输出测试详细的流程。

方式2:

package main

import "fmt"

func firmwarePackAccept(serveIp, fileName string, tcpPort uint64) string {
	// 建立TCP监听
	ipandPort := fmt.Sprintf("%s:%d", serveIp, tcpPort)
	return ipandPort
}

package main

import "testing"

func Test_firmwarePackAccept(t *testing.T) {

	type args struct {
		serveIp  string
		fileName string
		tcpPort  uint64
	}
	a:=args{
		serveIp: "192.1.1.122",
		fileName: "a.zip",
		tcpPort: 66,
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// TODO: Add test cases.
		{"a",a,"192.1.1.122:66"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := firmwarePackAccept(tt.args.serveIp, tt.args.fileName, tt.args.tcpPort); got != tt.want {
				t.Errorf("firmwarePackAccept() = %v, want %v", got, tt.want)
			}
		})
	}
}

写好方法以后,vscode右键Go: Generate Unit Tests For Function

image-20210915102613603

{"test", fields{start: true}, args{id: 0}},

Mutex锁、RWMutex锁和Once锁

golang中的sync包实现了两种锁:

  • Mutex:互斥锁
  • RWMutex:读写锁,RWMUtex基于Mutex实现

Mutex(互斥锁)

1、Mutex 为互斥锁,Lock() 加锁,Unlock() 解锁

2、在一个 goroutine 获得 Mutex 后,其他 goroutine 只能等到这个 goroutine 释放该 Mutex

3、使用 Lock() 加锁后,不能再继续对其加锁,直到 Unlock() 解锁后才能再加锁

4、在 Lock() 之前使用 Unlock() 会导致 panic 异常

5、已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁

6、在同一个 goroutine 中的 Mutex 解锁之前再次进行加锁,会导致死锁
适用于读写不确定,并且只有一个读或者写的场景

RWMutex(多读单写锁)

1、RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁

2、读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁

3、写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占
适用于读多写少的场景

Once锁

sync.Once是golang package中使方法只执行一次的对象实现,作用与init函数类型,但又不同:

  • init函数是在文件包首次被加载的时候执行,且只执行一次。
  • sync.Once是在代码运行中需要的时候执行,且只执行一次。

sync.Once使用变量done来记录函数的执行状态,使用sync.Mutex和sync.atomic来保证线程安全的读取done.

初始化变量、结构体。

https://geektutu.com/post/hpg-sync-once.html

sync.Map

map需要用make函数对其进行分配内存和初始化,sync.Map不需要初始化,用锁机制来保证并发安全。

    //开箱即用
    var sm sync.Map
    //store 方法,添加元素
    sm.Store(1,"a")
    //Load 方法,获得value
    if v,ok:=sm.Load(1);ok{
        fmt.Println(v)
    }

https://blog.csdn.net/u010230794/article/details/82143179?ops_request_misc=&request_id=&biz_id=102&utm_term=go

指针

  • golang提供了指针用于操作数据内存,并通过引用来修改变量。
  • 只声明未赋值的变量,golang都会自动为其初始化为零值,基础数据类型的零值比较简单,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要通过new开辟一个内存,或者通过make初始化数据类型,或者两者配合,然后才能赋值。
  • 指针也是一种类型,不同于一般类型,指针的值是地址,这个地址指向其他的内存,通过指针可以读取其所指向的地址所存储的值。
  • 函数方法的接收者,也可以是指针变量。无论普通接收者还是指针接收者都会被拷贝传入方法中,不同在于拷贝的指针,其指向的地方都一样,只是其自身的地址不一样。

推荐阅读