入门

本地项目和包管理

包管理

包.xxmodule-%USERPROFILE%\go

基本命令

/*
download    download modules to local cache (下载依赖的module到本地cache))
get         下载并编译
edit        edit go.mod from tools or scripts (编辑go.mod文件)
graph       print module requirement graph (打印模块依赖图))
init        initialize new module in current directory (再当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy        add missing and remove unused modules (增加丢失的module,去掉未用的module)
vendor      make vendored copy of dependencies (将依赖复制到vendor下)
verify      verify dependencies have expected content (校验依赖)
why         explain why packages or modules are needed (解释为什么需要依赖)
*/


//常用就是get、init、tidy

//例如初始化一个module:在cmd中运行:
go mod init "moduleName"

包加载的过程

  • 首先在main主包进入,通过递归的方式进行加载

基本语法

语法

基础

//var
var a = 12
//:=
a := 12
//var+类型
var a int = 12
var B = 13//全局变量

//该结构体全局可见
type Out struct {
    Name string //全局可见
    password string //包可见
}

//全局可见
func (o *Out) setPassword(password string) {
    o.password = password
}

//当前结构体包可见
type out struct {
    password string //包可见
}

//当前包可见
func (o *out) show() {
    
}

var bb = 15//当前包可见


func main() {
    var a = "aaaa"//局部变量
}
//当前包可见
func test1() string {
    return "private 函数"
}
//全局可见
func Test2() string {
	return "public 函数"
}
const (
	a = iota
    b
    c
    d
)
//a=0,b=1,c=2……
//全局常量
const CONST_NUM = "全局常量"
func main() {
	const ccc = "内部常量"
    var value = "string"
    var find = &value
    fmt.Println(*find==value)
}


//这时将不能直接给name通过取数值地址的方式赋值
type name struct {
	aaa *string
}


func init() {
    abc = "111111"
    test := name{
        // aaa : &"111"//这将不可行
        aaa = &abc
    }
}

func test(){
    var a = "aaaa"
    var b = 'b'
    var c = 12
    var d = false
    fmt.Printf("%s----%c----%d----%t",a,b,c,d)
}

变量

基本类型

  • 在go语言中,隐式类型转化被认为是不好的,所以不允许隐式类型转化,而涉及到不同的类型时需要显示类型转化,在go中数据类型比较复杂(简单的int就分为:int8、int16、int32、int64)
类型
type NewInt int //定义类型NewInt,其保存方式和int一样
type IntAlias = int //给int其别名
使用
//在go中,可以选择数字占用的位数
	//无符号数和有符号数分类一样
	//int、uint:默认占用机器字长的位数:64位机器占用64位、32位占用32位
var a1 int8 = 1 //var a1 uint8 = 1
var a2 int16 = 1
var a3 int32 = 1 
var a4 int64 = 1

//浮点类型只有32位和64位(相当于float和double
var f float32 = 1.1
var d float64 = 1.1111111

//布尔
var b1 bool = false
var b2 = true

//字符
var c1 byte = 'a'
var c2 rune = '中'

//字符串
var s1 = "aaaaaa"

make创建

var slice1 = make([]type,length,capacity)

var map1 = make(map[type]type,capacity)

var channel = make(chan type,capacity)
数组和切片
特点
//数组:值得注意的是range的遍历获取到的是下标,而不是value
	var nums = []int{1,2,3}
	for v := range nums {
		fmt.Println(nums[v])
	}
	var nums2 = [...][2]int{{1,2},{2,3}}
	for v := range nums2 {
		for vv := range nums2[v] {
			fmt.Println(nums2[v][vv])
		}
	}

//切片
	var slice1 = []int{}
	slice1 = append(slice1,1,2,3,4,5,6)
	fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>")
	fmt.Printf("%d:",len(slice1))
	//创建切片(足够大空间保存被复制)
	var slice2 = make([]int,10,10)
	//偏移2开始复制,返回复制量
	copy(slice2[2:],slice1)
	for v := range slice2 {
		fmt.Print(slice2[v])
	}
	//可以通过追加切片的方式进行非覆盖的复制,相当于有足够容量下,偏移到切片的末尾的复制
	slice2 = append(slice2,slice1...)
	slice1[0] = 999
	fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>")
	for v := range slice2 {
		fmt.Println(slice2[v])
	}
map
特点
key
使用
//map
func test1() {
	var m1 = map[string]int8 {
		"lili":12,
		"leilei":13,
	}
	//增删改查
	m1["lala"] = 111
	fmt.Println(m1["lala"])
	m1["lala"] = 122
	fmt.Println(m1["lala"])
    //有意思的是,delete没有返回值,所以无论删除存在都无法感知,需要辅助(例如先检查是否存在、检查删除前后map大小等)
	delete(m1,"lala")
	fmt.Println(m1["lala"])
    //获取value和是否存在value
	var mv1,has = m1["lala"]
	if has {
		fmt.Print(mv1)
	}
	for k1,v1 := range m1 {
		fmt.Println(v1,k1)
	}
    fmt.Println(len(m1))
}
channel
makeselectdefaultcasemakeclose

结构体

基础

byteruneint8int32int8或者int32结构体.和结构体指针->
//
type People struct {
    id int32
    name string
    address string
}

type student struct {
    school string
    class string
    people People //组合
    People //匿名的方式实现
}

type worker struct {
    city string
    work string
    people People
}

//是worker的成员方法,绑定worker
func (w worker) show() {
    w.city = "bbbbb"     //这里传入的是结构体的浅拷贝,所以普通数据类型修改后,返回后等于没有修改
    fmt.Printf("%v",w)
}
//是Student的成员方法,绑定Student
func (s *student) show() {
    s.school = "aaaaa"   //这里传入的是结构体指针所以修改在退出方法后仍然有效
    fmt.Printf("%v",s)
}
//是People的成员方法,绑定People
func (p People) show() {
    fmt.Printf("%v",p)
}

func main() {
	var t worker
	var p People
	t.show()
	p.show()
}

面向对象

type peoples struct {
	name string
	age  int
}

type student struct {
    //嵌套匿名结构体
    people
    class string
}

type worker struct {
    //嵌套有名结构体
    p people
    city string
}

func main() {
    var w = worker
    var s = student
    w.name = "1111"//直接当做当前结构体属性使用,
    s.p.name = "1111"//必须指定使用
}
package main
import "fmt"

type people interface {
	showId()
} 

type cus struct {
	id string
	people
}

func (c cus) input() {
	fmt.Printf("id:%s",c.id)
	c.showId()
}

type older struct {
	likes string
}

type young struct {
	likes string
}


func (y young) showId() {
	fmt.Printf("young like:%s",y.likes)
}

func (o older) showId() {
	fmt.Printf("ole like:%s",o.likes)
}

func main() {
	var c1 = cus{"1111",young{"eat"}}
	var c2 = cus{"2222",older{"sleep"}}
	c1.input()
	c2.input()
}	
	//例如数组
	var c = [...]int{1,2,3}
	var i interface{}
	i = c
	s := i.([3]int)
	fmt.Printf("%v,%d",c,len(s))
	//切片
	var c1 = []int
	var i1 interface{}
	c1 = append(c1,11)
	i1 = c1
	s1 := i1.([]int)
	fmt.Printf("%v,%d",c,len(s))
type peoples struct {
    name string
    age int16
    hobby string
}

func (p peoples) show1() {
	p.name = "123"
	fmt.Printf("%v\n", p)
}

func (p *peoples) show2() {
    //(*p),name = "123" //自动解引用后两者效果一样
	p.name = "123"
	fmt.Printf("%v\n", *p)
}

func main() {
    /**
    var peo = peoples {
        name : "lili",
    }
    
   	var peo = peoples {
        "lili",12,"eat",
        
    }
    */
    var peo = peoples{name:"lili"}
    //(&peo).show1(),这里在go的语法糖中会自动解析为peo.show1()
    peo.show1()
    //注意这里自动由于自动解引用,和(&peo).show2()效果是一样
    peo.show2()
}

公共父类接口

interface{}

接口

基础

interface{}Objectdefault方法、常量implementfunc (p *people) test() {}接口a*peoplepeople
type iface interface {
    eat()
}

type Integer int

func (i *Integer) eat() {
    
}

断言

func main() {
    var a A//接口类型A
    var peopel = People{"aaaa"}
    a = people//假设People实现了A接口
    
    var peo People
    peo,ok := a.(People)//类型断言,将a向下转为People类型
    if ok {
        //
    }
}

函数

//结果为1,defer修改了a
func test() (a int) {
    defer func() {
        a++
    }()
    return 0
}

流程控制

(){}whileforfor(;;)rangeswitchbreakfallthroughif、if elsebreak和continue

异常处理机制

deferdeferfinalpanicrecover
func printHello() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	//do假设这个函数抛出panic异常,就会被recover捕获;显然如果异常不处理就会终止程序
}

附录

  1. 格式化输出表(Printf)

  2. 内建函数

    函数名使用说明
    deletedelete(map,元素)删除map的元素,没有返回值,需要自主确认是否删除成功
    appendappend(切片,切片…) 切片/append(切片,若干元素) 切片用在切片的添加
    lenlen(数组\切片\map\channel) int元素的数量
    capcap(切片\channel) int容量
    closeclose(channel)关闭通道
    makemake(type,len,cap) type创建切片、map、channel
    newnew(type) type分配内存,但是不会初始化该内存
    complex
    imag
    real
    Unsafe.SizeofUnsafe.Sizeof(type)获取大小
    Unsafe.OffSet
    Unsafe.Alignof
    panic和recover
    print和printf

语法糖

defer
defer func_defergoroutine
go和select
  • 开启协程,在并发编程中详细介绍

<-
  • 在数据结构channel中详细介绍,就是channel的接收和发送
接受者自动解引用和引用
type testInterface interface {
	test()
}

type testInterface1 interface {
	test1()
}

type test11 struct {
	name string
}
//指针类型接受者
func (t *test11) test() {
	fmt.Println(t)
	t.name = "lili2"
}
//值类型接受者
func (t test11) test1() {
	fmt.Println(t)
	t.name = "lili1"
}

func main() {
	t := test11{name: "lilei",}
	var f1 testInterface
	var f2 testInterface1
	f1 = &t//必须是指针类型
	f2 = t //等价于f2 = t
	f1.test()
	f2.test1()

}

并发编程

  • 前面都是介绍go的基本概念(就是怎么适应go)、go的强大特性并不是体现在前面的简介的代码上,而是在并发编程上

协程

go 函数goselect - case - defaultdefaultbreakcasecase/defaultdefault
var channel = make(chan string,3)
func hello(name string) {
    channel <- name
   	fmt.Println("hello", name)
}

func bye() {
    name := <-channel
    fmt.Println("bye", name)
}

func main() {
    go hello("lili")
    go bye()
    time.Sleep(1111)
    close(channel)
}

其他

单元测试

XXX_test + testing + go testXXX_testtestXxxxx
//假设这是一个abc_test
func testName(t *testing.T) {
    //测试的函数
    res := name()
    if res== `a` {
        t.Fatalf("执行错误")
    } 
    t.Logs("执行成功")
}

//在文件目录执行go test

反射

Classinterface(value,type)typevaluetypeunsafe.Pointer

T

// TB is the interface common to T, B, and F.
type TB interface {
	Cleanup(func())
    //错误
	Error(args ...any)
	Errorf(format string, args ...any)
	Fail()
	FailNow()
	Failed() bool
    //停止
	Fatal(args ...any)
	Fatalf(format string, args ...any)
	Helper()
    //日志
	Log(args ...any)
	Logf(format string, args ...any)
	Name() string
	Setenv(key, value string)
	Skip(args ...any)
	SkipNow()
	Skipf(format string, args ...any)
	Skipped() bool
	TempDir() string

	// A private method to prevent users implementing the
	// interface and so future additions to it will not
	// violate Go 1 compatibility.
	private()
}

网络编程

  • 在go中网络编程和并发编程都像是封装好了的(有框架)的java的网络编程和并发编程一样;(就是和JUC、Netty差不多),在go的net包中支持
    • rpc、Http、mail等
    • socket编程:UDP、TCP

Socket

基础

type Conn interface {
	//读写
   Read(b []byte) (n int, err error)
   Write(b []byte) (n int, err error)
	//关闭
   Close() error

	//连接信息
   LocalAddr() Addr
   RemoteAddr() Addr

	//短连接的读写超时(想秒后主动关闭连接)
    	//相当于延迟Close
   SetDeadline(t time.Time) error
    	//变为半连接,主动发起关闭
   SetReadDeadline(t time.Time) error
    	//变为不再发送
   SetWriteDeadline(t time.Time) error
}

心跳机制

SetKeepLivegoroutine

Http

  • 在go中提供了原生的基于路由类型的网络编程的支持,所以原生的go就支持Route框架
    • Route框架:net/http
    • MVC框架:gin等

Route框架

  • 在http的Route框架下,除了不支持动态的解析和绑定视图外(基本MVC功能都有),但是go没有支持通配符
    • route、handler:路由和路由处理器(类似spring的路由和controller)
    • 连接池:在一次完整的http请求中保存获得TCP连接(go支持连接池)
    • request、response:go封装了request和response
服务端
  • 在go中相当于内置了一个小的如有框架+TCP默认管理器
ServerMux
  • 该对象的作用是:作为一个一个 HTTP 请求多路复用器
    • 路由匹配:使用最佳匹配
    • 重定向
    • 调用handler:根据路由匹配获取handler
ServeMux 是一个 HTTP 请求多路复用器。它将每个传入请求的 URL 与已注册模式列表进行匹配,并为与 URL 最匹配的模式调用处理程序。
模式名称固定,有根路径,如“/favicon.ico”,或有根子树,如“/images/”(注意尾部斜杠)。较长的模式优先于较短的模式,因此如果同时为“/images/”和“/images/thumbnails/”注册了处理程序,则会为以“/images/thumbnails/”开头的路径调用后一个处理程序,而前者将接收对“/images/”子树中任何其他路径的请求。
请注意,由于以斜杠结尾的模式命名了根子树,因此模式“/”匹配所有其他注册模式不匹配的路径,而不仅仅是具有 Path ==“/”的 URL。
如果已注册子树并且接收到命名子树根但没有尾部斜杠的请求,则 ServeMux 将该请求重定向到子树根(添加尾部斜杠)。可以通过单独注册不带斜杠的路径来覆盖此行为。例如,注册“/images/”会导致 ServeMux 将对“/images”的请求重定向到“/images/”,除非“/images”已单独注册。
模式可以选择以主机名开头,将匹配限制在该主机上的 URL。特定于主机的模式优先于一般模式,因此处理程序可能会注册两种模式“/codesearch”和“codesearch.google.com/”,而不会同时接管对“ http://www.google.com/  ”的请求”。
ServeMux 还负责清理 URL 请求路径和 Host 标头,剥离端口号并重定向任何包含 .或 .. 元素或重复斜杠到等效的、更清晰的 URL。
ServeMux 还负责清理 URL 请求路径和 Host 标头,剥离端口号并重定向任何包含 .或 .. 元素或重复斜杠到等效的、更清晰的 URL。
使用
  • 基本使用
func main() {
    //绑定路由
	http.HandleFunc("/hello", helloHandler)
    //绑定端口,nil表示使用默认的http处理
	err := http.ListenAndServe("127.0.0.1:8999", nil)
	if err != nil {
		fmt.Println("检查输入")
	}
}

//处理函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
	var res = "hello world"
	_, err := w.Write([]byte(res))
	if err != nil {
		fmt.Println("输出有误")
	}
}
标椎库

internal包

  • 该包是go的基础包,定义了go内部静态实现的数据结构

unsafe包

基础

Pointer的机制

Pointer

func Float64bits(f float64) uint64 { 
    return (uint64)(unsafe.Pointer(&f)) 
}
// 结构体地址运算 f := unsafe.Pointer(&s.f)
 f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

 // 数组地址运算 e := unsafe.Pointer(&x[i])
 e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

//错误的地址运算
	// 将地址移到内存末尾
 var s thing
 end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
 b := make([]byte, n)
 end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
	//不再一个表达式/对象为nil
 u := unsafe.Pointer(nil)
 p := unsafe.Pointer(uintptr(u) + offset)

API

ArbitraryType IntegerType type ArbitraryType inttype IntegerType intSizeofOffsetofAligonfAddunsafe.Pointer(uintptr(unsafe.Pointer(&xxxxx)) + unsafe.Offsetof(XXXXX))Slice

补充

unsafeheader

string和slice实现

unsafe.PointerLen
type Slice struct {
	Data unsafe.Pointer
	Len  int
	Cap  int
}

type String struct {
	Data unsafe.Pointer
	Len  int
}

工具类型包

time包

time

数据解析类型包

并发包

syncsync/atomiccontextchannel

sync包

GC

sync/atomic

基础

Valueany

原子无锁操作

  • atomic无锁操作支持:int32、int64、uint32、uint64、uintptr、unsafe.Pointer,六种数据类型
  • 主要的无锁原子操作有:swap、cas、Add、Load和Store五种类型
  • API如下

Value

  • Value是扩展的(类似java原子变量)的数据结构,提供了四种原子无锁操作方法

context

特点

退出通知元数据传递contextgoroutinechannel+select

使用

ctx context.todo
package main

import (
	"context"
	"fmt"
)

func main() {
	//根context
    ctx := context.Background()
	
		//获取一个可以传递参数的context
    valCtx := context.WithValue(ctx, "id", "333333")
    	//传递context
	go value(valCtx)
    
    	//获取一个超时取消的Context,假设10秒后自动取消
    timeCtx, cancelFunc := context.WithTimeout(ctx, time.Second*10)
	defer func() {
        //假设30秒后有人掉线
        time.Sleep(time.Second*15*2)
		cancelFunc()
		fmt.Println("final")
	}()
    go timeOut(timeCtx)
}

func value(ctx context.Context) {
	id, ok := ctx.Value("id").(string)
	if ok {
		fmt.Printf(id)
	} else {
		fmt.Printf("no id")
	}
}


//假设这个超时超时取消任务是:两人联机,每秒获取两人的位置,再想双方发送位置
	//获取方式是
		//超时获取:即对局内有效,对局结束自动关闭
		//可以取消;当连接断开时不再获取,退出
func timeOut(ctx context.Context) {
    for {
        getStand()
        sendStand()
        select {
            case <-ctx.Done(): {
                return
            }
            case <-time.After(time.Second): 
            
        }
    }
}

实现

ValueDoneDeadlineErrValueDoneDeadlineErrcancelDoneemptyCtx根contextBackgroundTODOcancelCtxWithCanceltimerCtxWithTimeoutvalueCtxkey valueWithValue

实现

type timerCtx struct {
    //继承cancelCtx
	cancelCtx
	timer *time.Timer //定时器,看前文
	deadline time.Time//截止时间
}

type cancelCtx struct {
	Context

	mu       sync.Mutex            // 同步锁,前文
	done     atomic.Value          // chan struct{},懒惰地创建,由第一次取消调用关闭
	children map[canceler]struct{} // 在第一次取消调用时设置为 nil
	err      error                 // 由第一次取消调用设置为非零
}



//只有key、value
type valueCtx struct {
	Context
	key, val any
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

//d:进入的时候的时间
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	//...
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

运算包

  • regexp:正则表达式
  • math:数学包
  • math\big:大数运算

sort

  • 这个类似java的Arrays标椎库,可以进行排序(数组、切片、接口);

    • 默认升序排序、可以通过重写len、less、swap实现倒叙排序、结构体排序
  • 提供了排序、二分查找(查找不到返回插入位置)、

encoding

func mian() {
	marshal, err := json.Marshal(peo)
	err = json.Unmarshal([]byte(marshal), &peo)   
}

系统类型包

io和os

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)
实现
  • 下面将根据《go语言设计与实现》大概学习一下基本实现思想(对比Java),在并发编程中再根据源码深入学习

编译

  • 在go语言中,是编译性语言,不像Java通过class文件再由虚拟机解释运行,这使得在go中具有以下特征
    • java的类加载机制在go中编译前就需要完成
      • 基本语法检查机制
      • 变量的初始化
      • 外部依赖导入
      • 变量主体检查、内联、闭包
    • 为了实现高级特性(例如逃逸分析,栈上分配等)编译成二进制机器语言前需要进行一次汇总分析

编译的过程

AST

TOKEN序列

中间代码

生成
make、new、selectruntimemakechan<-

运行

  • go的最大特点就是运行时是真正面向并发的,而且其兼顾了面向对象的特性,这使得go具有两个方面的特征
    • CPU调度管理上:呈现面向并发(栈区、用户态调度)
    • 内存分配与回收:呈现面向对象(堆区)

CPU

  • go完全实现了协程,在go中的并发编程是基于协程进行的;所以go提供了用户态下调度需要的:
    • 上下文
    • 调度器机制
    • 锁机制
    • 通信机制

上下文

  • 在Java中线程和其创建的子线程是具有关系的,但是在go中所有goroutine是平级的(除了主goroutine),为了实现在这些没有关系的goroutine关联,go提供了一种非常简便的机制:上下文context
    • 和Java的inheritableThreadLocal非常相似,都是为了子goroutine/thread可以和主线程通信或者传递信息
  • 在go的context中:通过channel传递信息(天生就实现线程安全)、通过value传递参数,但是value会不会有inheritableThreadLocal的线程安全问题呢?

调度器机制

基础
  • go的调度器机制本质就是在用户态下模拟线程调度,这点和Java的Executor/AQS框架非常像(但是Executor框架目的是线程复用,重点并不在调度),go中重点是调度,可以说go的协程机制就是加上了调度的Executor;
  • 为了实现调度,在go中提出了一个调度模型:GMP
    • G就是goroutine,除了作为一个task需要的状态信息外、还有模拟虚拟机的调度的运行信息(PC计数器、栈帧、栈指针等);为了实现用户态调度控制:调度信息,可以通过复用g实现协程池的概念
      • 这个相当于在AQS的Runnable/Callable的超级加强版,特别的是goroutine允许复用,在AQS中完全不可能有这种概念
    • MM是操作系统线程的抽象,在GMP模型中,一个M唯一对应一个操作系统线程、唯一对应一个处理器;除了抽象操作系统线程的基本信息(类似Java的Thread的信息外),其还有模拟AQS的LockSupport:即用户态下保存的所有锁、调度、同步消息和方法
      • 在Executor相当于worker(工作线程)和LockSupport的组合的加强版
    • P:调度器的处理器,这里涉及到了真正实现协程的原理,除了具有AQS的一般功能和字段信息,P作为处理器还需要模拟真正CPU调度需要的性能和计数器字段
      • 在Executor中相当于加强版的AQS
G
  • 和AQS一样,本质就是将线程状态在用户态模拟实现,所以主要有以下状态:可执行、执行、阻塞(等待)、死亡

  • 在go中goroutine非常轻量级,只需要通过go调用函数即可开启,goroutine复用可以实现协程池的概念;

  • go中创建goroutine过程:

    • 获取或创建goroutine对象(go中内置了协程池以实现复用goroutine)
    • 通过传入参数初始化goroutine上栈保存的数据(局部变量表、退出信息、引用信息等)
    • 设置goroutine参数(设置goroutine一系列属性:调度信息、栈信息、计数器信息等)
    • 加入运行队列(局部/全局)
M
  • 本质就是一个循环不断执行的线程,M被创建出来后就会向Executor的worker一样不断的处理goroutine;
  • go的调度器中有两级调度队列,所以在查找调度的goroutine的时候会有特别的处理点
    • 需要尽可能保证公平
    • 最大化利用CPU利用率
      • 两级调度队列获取:之所以设计两级调度队列是因为:当前线程的局部队列调度是不需要加锁的、调度全局队列都是需要加锁的,go为了优化所以使用了两级调度
      • 窃取:为了尽可能让goroutine整体比较均衡,允许窃取其他线程的局部调度队列的goroutine
P
goroutine

内存管理

基础

  • 在go的内存管理中和Java虚拟机非常相似
    • go非常希望通过栈上分配减少对于堆的内存申请(jvm也一样)
    • go的堆分配策略和jvm相比有一定改进(和保留一定进步空间)
      • go和jvm一样都会分配一部分空间作为当前线程独立分配堆区以避免分配空间时加锁
      • go引进了多级分配策略(分级分配),这种策略极大的优化了go的分配速度
      • go没有分代收集,所以也没有使用标记复制算法(而是标记清除),所以go非常希望栈上分配小对象以避免出现GC问题;同时这种算法避免了复制以及对于对象移动带来的栈区引用修改,加快了垃圾回收的速度;
      • 由于go没有分代收集,为了避免进行紧凑,使用了隔离适应的策略;
    • 垃圾回收(和JVM的CMS类似)
      • go的runtime同样使用经典的三色标记法进行标记,使用的策略是增量更新,通过写屏障实现增量更新保存(过程CMS差不多,分为四个阶段:初始标记、并发标记、重新标记、并发清除),具体看JVM
    • 总的来说就是:go的内存分配策略类似G1的方式回收策略类似CMS的方式(因为其没有分代收集);go希望尽可能通过栈上分配避免小对象出现在堆区

多级分配

对象
  • go中将对象分为3种大小:微对象、小对象、大对象;
    • 微对象(0-16B):一般就是逃逸的变量和小的字符串但是不能是指针类型,这种对象一般保存在线程缓存中,分配和收集都很快速;
    • 小对象(16-32KB):这种对象需要通过spanClass进行管理,可能被分配到线程缓存、中心缓存或者堆页中
    • 大对象:直接分配到堆页中
三次缓存
  • 线程缓存(mcache):线程缓存只属于一个线程,所以在其中分配不需要加锁
  • 中心缓存(mcentral):所有线程公用的缓存,在其中分配显然需要进行加锁,
  • 页堆(heap area):每个heap area会占用一个page

GC

内存泄漏

  • 在java中,内存泄漏一直是GC学习的重点,尽管jvm已经帮我们避免了大量的这种情况,但是在go中却非常危险
    • 切片机制
      • 获取长字符串中的一段导致长字符串未释放
      • 获取长slice中的一段导致长slice未释放
      • 在长slice新建slice导致泄漏
    • 协程
      • goroutine泄漏
      • time.Ticker未关闭导致泄漏
    • Finalizer导致泄漏
    • Deferring Function Call导致泄漏

go的gc

  • 说到内存泄漏一定要知道go到底是怎么分配内存的,或者说go怎么实现gc中第一步查找回收对象
    • 前面我们知道go通过三色标记+增量更新的策略确定可回收和不可回收的对象,那么gc-root怎么来的呢
  • 位图标记:在go中没有像java一样有虚拟机(VM),所以这要求进行内存分配必须通过统一的方式,在该方式上为GC作标记工作,以查找GC-ROOT,实现三色标记法,在go中就体现为位图标记;

确定回收对象

GC-Root
mallocgc
  • mallocgc函数是go分配内存的唯一方法(除非通过cgo调用c获取内存),也是实现gc的基础,通过mallocgc统一分配才能确定GC-Root(在JVM中GC-root就有由:OoMap+RSet得到)以作三色标记
    • 辅助GC
    • 空间分配
    • 位图标记:位图标识是非常重要的一步,决定了GC的回收(和发生内存泄漏)的场景
    • 其他收尾
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	
	//第一阶段:检查辅助gc
	var assistG *g
	if gcBlackenEnabled != 0 {
		assistG = getg()
		if assistG.m.curg != nil {
			assistG = assistG.m.curg
		}
		// 积累信用
		assistG.gcAssistBytes -= int64(size)
		//欠债必须辅助gc
		if assistG.gcAssistBytes < 0 {
			gcAssistAlloc(assistG)
		}
	}

	// Set mp.mallocing to keep from being preempted by GC.
	mp := acquirem()
	if mp.mallocing != 0 {
		throw("malloc deadlock")
	}
	if mp.gsignal == getg() {
		throw("malloc during signal")
	}
	mp.mallocing = 1

	shouldhelpgc := false
	dataSize := userSize
	c := getMCache(mp)
	if c == nil {
		throw("mallocgc called without a P or outside bootstrapping")
	}
	var span *mspan
	var x unsafe.Pointer
	noscan := typ == nil || typ.ptrdata == 0
	// 分配空间
	delayedZeroing := false
    //小于32k
	if size <= maxSmallSize {
        //小于16B
		if noscan && size < maxTinySize {
			//还有足够空间,直接在mcache分配
			if off+size <= maxTinySize && c.tiny != 0 {
				x = unsafe.Pointer(c.tiny + off)
				c.tinyoffset = off + size
				c.tinyAllocs++
				mp.mallocing = 0
				releasem(mp)
				return x
			}
			// 否则再申请一个新的mcache,然后进行分配
			span = c.alloc[tinySpanClass]
			v := nextFreeFast(span)
			if v == 0 {
				v, span, shouldhelpgc = c.nextFree(tinySpanClass)
			}
			x = unsafe.Pointer(v)
			(*[2]uint64)(x)[0] = 0
			(*[2]uint64)(x)[1] = 0
			if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
            //全局缓存分配
		} else {
			var sizeclass uint8
			if size <= smallSizeMax-8 {
				sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
			} else {
				sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
			}
			size = uintptr(class_to_size[sizeclass])
			spc := makeSpanClass(sizeclass, noscan)
			span = c.alloc[spc]
			v := nextFreeFast(span)
			if v == 0 {
				v, span, shouldhelpgc = c.nextFree(spc)
			}
			x = unsafe.Pointer(v)
			if needzero && span.needzero != 0 {
				memclrNoHeapPointers(unsafe.Pointer(v), size)
			}
		}
        //堆中分配
	} else {
		shouldhelpgc = true
		span = c.allocLarge(size, noscan)
		span.freeindex = 1
		span.allocCount = 1
		size = span.elemsize
		x = unsafe.Pointer(span.base())
		if needzero && span.needzero != 0 {
			if noscan {
				delayedZeroing = true
			} else {
				memclrNoHeapPointers(x, size)
			}
		}
	}

	var scanSize uintptr
    //步骤三:标识位图
	if !noscan {
		heapBitsSetType(uintptr(x), size, dataSize, typ)
		if dataSize > typ.size {
			if typ.ptrdata != 0 {
				scanSize = dataSize - typ.size + typ.ptrdata
			}
		} else {
			scanSize = typ.ptrdata
		}
		c.scanAlloc += scanSize
	}
    
    //收尾工作
    	//检查是否为增量更新的gc需要添加扫描节点
	publicationBarrier()
	if rate := MemProfileRate; rate > 0 {
		if rate != 1 && size < c.nextSample {
			c.nextSample -= size
		} else {
			profilealloc(mp, x, size)
		}
	}
	mp.mallocing = 0
	releasem(mp)
    	//检查是否触发GC
	if shouldhelpgc {
		if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
			gcStart(t)
		}
	}

	return x
}
heapBitsSetType
RememberSet

PS:后续所有开源学习笔记同步到gitee,有需要去拉取 https://gitee.com/wusport/open-source-notes