golang一文入门

入门

本地项目和包管理

包管理

  • go和java不同的是,在java中方法是通过类来管理的,仓库管理的是类;而在go中:函数是一级公民,但是函数不会(也没法)被作为一级公民管理;所以go的包管理【包这时就像Java的类一样,此时函数就像static方法】,所以包管理不是管理某一个源文件,而是包下所有源文件,换句话说就是一个包下
    • 所有源文件相当于一个源文件
    • 源文件:变量、方法、结构体、函数、接口都是共享的(换句话说就是一个包下的所有源文件不允许有重复的上述的部分)
    • 在go中调用仅仅需要import,然后即可通过包.xx使用,这时包名就像Java的类名一样,导入包时就像java导入类一样;
  • 现在go使用module进行包管理,这种方式下有以下规则(规范)
    • 包名用小写不要使用-、且和文件夹名相同
    • 一个文件夹下的所有源文件属于同一个包
      • Windows默认工作目录:%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"

包加载的过程

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

基本语法

语法

基础
  • 标识符【即变量】(和java一样:命名只能是数字字母下划线)的定义(三种)

    //var
    var a = 12
    //:=
    a := 12
    //var+类型
    var a int = 12
    
  • go中:首字母大写表示全局可见、首字母小写表示当前包可见(无论是源文件的变量、结构体的变量、方法或者函数

    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 函数"
    }
    
  • 常量:和c一样通过const定义、支持通过iota实现常量计数器的功能(弥补了没有枚举的弊端)

    const (
    	a = iota
        b
        c
        d
    )
    //a=0,b=1,c=2……
    
  • 指针:和C语言一样***、&、有意思的是在go中常量是不允许取地址的**;这对于结构体赋值有非常大的影响例如

    //全局常量
    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
        }
    }
    
    
  • 输出:在go中输出和C语言基本一样,可以格式化输出(Printf)、如果不指定格式化输出就使用原生输出(这时类似byte会输出int8,因为其通过int8保存

    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通过内置函数来操作内置的复杂的数据类型(切片、map、channel),例如make、range等;有意思的是range遍历的时候获取到的是key(或者说下标)

  • 标签:在go中为了方便进行json序列化和反序列(数据库字段别名等),可以给结构体的字段加上标签,然后在通过反射机制就可以实现变量名的转化

变量

基本类型
  • 在go语言中,隐式类型转化被认为是不好的,所以不允许隐式类型转化,而涉及到不同的类型时需要显示类型转化,在go中数据类型比较复杂(简单的int就分为:int8、int16、int32、int64)
类型
  • 普通变量

    • 整形、无符号数、长整形、浮点类型
    • 字节、布尔
      • 在go中,字节有两种存在形式:byterune其中byte对应java的char、rune对应的是utf-8的字符(即包含除了ASIC码以外的中英文),在go中本质保存的是int8和int32
      • 布尔
    • 字符串
  • 特殊变量

    • complex64 complex128复数类型
    • uintptr :地址类型,相当于void*uintptr 是 Go 内置类型,表示无符号整数,可存储一个完整的地址。
    • 指针:在go中的指针变量和C语言的使用方法基本一样,但是go中绝对不允许对指针进行偏移和运算所以不能类似C语言一样通过数组指针操作数组(只能通过保存数组每一个元素的指针数组实现),但是结构体可以通过. ->运算【主要是结构体非常特殊,必须支持运算】;
  • 类型

    • 在go中允许通过type类型定义类型别名实现指代原生类型,和c的typedef一样

      • 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创建
  • 在go中允许通过make创建切片、map和channel这三种类型,但是不允许通过其创建其他类型

  • make的作用主要就是分配一个保存切片、map、channel信息的数据结构(结构体)

  • 这三者是利用结构体实现,所以他们本身是复杂数据类型,可以认为就是Java中的对象一样

    • var slice1 = make([]type,length,capacity)
      
      var map1 = make(map[type]type,capacity)
      
      var channel = make(chan type,capacity)
      
数组和切片
特点
  • 字符串

    • 类似Java,数组长度都是不可变的
    • 但是和java不同的是在Go中和c一样数组长度编译时就必须确定,换句话说就是不允许通过变量设置数组长度;
    • 数组长度无论是动态推导还是指定在编译时必须确定、而且在go中没有类似C语言的malloc申请空间,然后通过指针实现运行时确定大小的数组,所以如果使用数组将非常麻烦,不过go有另外一套机制实现动态数组:切片
  • 切片

    • 类似Java的动态数组(ArrayList),是可变长的,有意思的是:可以认为切片的动态扩容是渐进的,因为其实现是本质就是数组,共享创建切片的数组、独占append的部分
      • 切片内置函数:
        • append(slice,元素,元素,元素【……】)或者append(slice,slice1...)
          • 这两种方式都是往切片添加元素,返回slice,而且这种追加是元素拷贝加入
        • cap(slice)len(slice)切片容量和长度
        • copy(toSlipe,fromSlipe):在go中copy函数专门用于切片的copy,需要说明的是go中切片的copy本质就是元素复制,而且是通过覆盖的方式复制,所以
          • 复制期间不会动态扩容,所以必须创建一个容量足够的切片
          • 复制是由前到后顺序进行(可以通过偏移决定开始复制的位置和开始被覆盖的位置)
          • 被复制的数组长度大于覆盖数组时,将不再复制,不会进行动态扩容
    //数组:值得注意的是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
特点
  • 有意思的是在go中,map属于内置类型,除此之外,出于某种考虑,map和java的map还有以下的不同(在go中的map类似脚本语言)
    • delete操作,在go的delete中,无论是否有对应待删除的key都不会有返回值
    • 通过read【直接类似数组操作】包含java的containsKey,换句话说就是:无论读取的key是否存在都可读,如果不存在返回零值(例如bool对应false)
    • 考虑到零值可能被占用,所以go提供了另外一种返回值形式(在go中函数允许有多返回值),这时有两个返回值:一个是读取的、一个标识key是否存在的bool类型
    • 在go中,没有获取keys或者values的方法,因为作为内置的类型,没这个必要(使用者可以通过range遍历获取【类似数组】
使用
//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
  • 管道是一个线程安全的可以并行读写(一个读、一个写)的双向的通道,go推荐通过管道进行协程间的通讯;
    • 有时候为了让安全可能只想暴露读/写:就可以设置管道为只读/只写
  • 管道的容量(不会扩容)默认为0,在make的时候可以自定义
    • 当容量满时:阻塞写入
    • 当没有元素时:阻塞读取
    • 可以通过select机制避免一直阻塞
      • 非阻塞:直接使用default
      • 超时阻塞:case校验超时时间
  • 管道有俩种基本状态:打开和关闭,关闭后将不再允许写入,但是可以读取
    • 打开:make后就处于打开状态
    • 关闭:close:立刻使得写入关闭【换句话说就是通道变为只读】,但是读取还是可读(通过检查读取会发现仍然是打开状态),当通道为空时才算关闭了通道

结构体

基础
  • 结构体就是一个go内置的类型,通过type给struct起类型别名可以实现类似面向对象的对象的概念(和c一样)
    • type:主要由于类型起别名,例如在go中没有byterune,这两者分别是int8int32的类型别名,编译后本质保存的是int8或者int32
  • 表现为基本数据类型的性质:结构体和其他基本数据类型一样,在go中作为参数传递的是浅拷贝的数据(除非传递的是指针),这一点和Java有很大的区别,在Java中传递的是引用而这个引用直接拷贝是操作数栈的【换句话说就是对象这种复杂类型本质穿的是地址】,但是在go中,结构体不认为是复杂数据类型,这使得其传递是浅拷贝到其中每一个元素
  • 在go中,结构体和c结构体的思想一致和基本的使用(比如 结构体.和结构体指针->两种访问内部属性方式),数据和程序分离,其c又略微不同;在go中有面向对象的思想的支持,后面重点介绍
//
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()
}
面向对象
  • 接口和组合

    • go中有组合匿名结构体的概念【go认为组合优于继承、go通过组合实现复用】,通过匿名结构体继承该结构体的所有属性和方法,需要说明的是这种组合的情况下不会有继承带来的多态的概念(就是不会有方法覆盖)

      • 嵌套匿名结构体(模拟继承)

        • go中语法糖可以让匿名结构体可以像继承一样使用(包括属性和方法)
        • 但是在模拟多继承(组合多个结构体),而且存在冲突(变量名或者方法名)则需要明确指出使用的组合的结构体
        • 允许嵌套基本数据类型
      • 嵌套有名结构体(组合)

        • 有名结构体就是简单的组合关系,本质作为结构体的一个属性
        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"//必须指定使用
        }
        
    • go的接口非常重要,由于go不支持继承、这时需要一种机制来实现扩展性,这种机制就是接口,有意思的是在go中接口居然不需要显示的实现

      • 面向对象编程中多态是非常重要的功能,多态使得程序的复用和解耦合得以实现,但是多态是继承带来的优点,go只能组合;考虑到多态本质就是持有持有该类型方法的引用地址进行调用,在go中可以通过公共结构体(复杂数据类型)的interface实现多态;

        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()
        }	
        
      • 所以在go中接口不仅仅是方法的抽象,而且所有复杂数据类型和结构体都默认实现了空接口,换句话说就是空接口类型时所有类型的父类型,类似java的Object

      • 通过断言可以将空接口转化为具体类型,一定程度上弥补了没有继承带来的问题

        • 	//例如数组
          	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))
          
  • go支持反射,所以在结构体中有tag的概念,可以通过反射获取tag,以方便操作结构体的属性

  • 支持方法,不过go的方法稍微有点区别

    • go的结构体支持成员方法,这一点将和c完全不同,使得结构体具有一定的对象的性质(但是还是要说,go中,结构体是具有基本数据类型性质的);

    • 在go的语法糖中默认实现了结构体所有字段的构造方法,即通过类似json赋值一样对任意字段进行复制初始化结构体;

    • go的成员方法也是通过是否为首字母大小写确定权限范围(大写相当于public、小写相当于protect:包内可以访问)

    • **go的结构体中没有隐式变量(this)的概念,通过和结构体绑定的成员方法自定义this;**这个绑定就是所谓的接受者,接受者类型决定了是否需要浅拷贝数据(普通类型)还是拷贝引用(指针)

    • 需要说明的是go的方法相当于java的static方法,所以在go中的方法如果传入(绑定)的是普通结构体而不是结构体指针的话,修改返回后将不生效;

    • 在go中会自动解引用,换句话说就是传入结构体指针的使用方式可以当做结构体直接使用

      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{}
  • String

接口

基础
  • 前面大概介绍了结构体通过
    • 函数绑定变成方法,实现面向对象的方法的概念
    • 组合实现面向对象的复用
    • 接口实现多态,并且利用一个空接口interface{}作为类型Object一样,作为所有结构体祖先
  • 就接口而言,和面向对象的接口具有以下不同点
    • 接口本身可以通过组合(匿名)的形式实现接口的继承
    • 接口内完全不支持类似default方法、常量
    • 接口是通过隐式方式被结构体实现,不需要implement
    • 自定义类型(type定义的所有类型,包括结构体等)可以实现多个接口
    • 在接口的实现方法中,必须明确的是:方法的接受者类型时是什么类型。就是什么类型实现了该接口;换句话说就是:func (p *people) test() {}假设这个接口是接口a定义的接口,那么这个接口的实现类是*people不是people
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 {
        //
    }
}

函数

  • go的函数允许多返回值(一般只用在返回错误的时候使用)

  • 在go中函数就相当于与面向对象的类,是一级公民;

    • 所以和面向对象的类一样有几种类型:匿名函数、公共函数、方法(就绑定在结构体上,使得结构体具有对象的概念,类似面向对象的static方法)
    • go支持函数指针,而且比C语言更好理解,因为函数作为一级公民,函数指针可以让结构相同(参数和返回值党的类型、数量,位置一样)时,这两个函数在寻址时是一样的(类似面向对象的接口类型可以作为所有实现类的引用类型,实现复用)
    • 作为一级公民显然可以作为参数、返回值进行传递
    • go不支持函数嵌套(但是类支持内部嵌套子类,再由外部类调用进入嵌套类调用嵌套类),但是可以使用匿名函数、再通过函数指针返回的到匿名函数(实现嵌套效果)
    • go的函数存在闭包的概念,闭包=函数+环境,这点和Java的内部类有很大的不同,在Java中只有final变量才能在匿名内部类使用(本质就是扩展生命周期,但是为了一致性不允许修改,表现为浅拷贝),在go中函数是完全支持闭包的,换句话说就是共享环境是可以动态修改的且作用域是在可以被引用的整个生命周期
  • go严格规范了代码,所以对于有返回值的函数、无论有多少个返回值都必须接受,前面说到,定义的变量又必须使用,为了解决这里的矛盾(部分返回值可能用不到)可以使用_接受即可

  • go函数和java方法一样支持变长参数、例如func add(nums...int) int {},这时nums数组长度由变长函数输入的个数确定

  • go的函数可以给返回值指定名称(绑定名称),这是因为go存在defer机制,可以在defer中修改具名返回值的结果

    • 在go中是:return分为两步(这点和Java类似):

      • 将return的值赋值给return变量(无名的话就隐式)
      • 然后return这个返回变量
    • 在go中:defer可以通过修改具名返回值变量修改返回值

      //结果为1,defer修改了a
      func test() (a int) {
          defer func() {
              a++
          }()
          return 0
      }
      
  • 在go中传递是浅拷贝的传递,这点和java一样(但是需要注意结构体,因为在go中结构体是非常特别的,他作为基本数据类型,相当于java浅拷贝重写clone,使得拷贝的是结构体内所有的数据形成新的拷贝(而不是拷贝结构体指针))

  • 每个源文件的执行顺序都是:初始化变量(及初始化变量调用的函数)->init函数->main函数->被调用函数,这一点和java基本一致,不同在于

    • init函数相当于构造函数、但是只能是无参的
    • 可以有多个
    • 一个源文件可以如果有多个init函数没有规定其中执行顺序

流程控制

  • 在go中,流程控制语法很有意思:
    • 不允许有()包括
    • 强制规定{}符合换行规范
    • 没有while、而且没有三元运算
  • 在go中for允许无条件或者指定为true,这种情况下相当于 java的for(;;);除此之外,go中还有一点就是,类似Java,允许迭代器变量(range
  • go的**switch也很有意思(和其他语言不同),不需要break默认自动会break,如果需要继续使用的是fallthrough表示继续下一条语句;和其他语言一样支持变量匹配和表达式匹配**
  • **if、if else**和其他语言基本一样,除了没有括号
  • 支持break和continue
  • go支持基本的运算但是
    • 不支持三元运算
    • 不支持无符号右移
    • go的标椎库没有翻转字符串/切片/数组的函数

异常处理机制

  • go的一个比较麻烦的是其异常处理,在go中不会抛出异常(不鼓励抛异常),是通过返回错误的形式返回异常,go中的返回值又必须接受
  • go和java一样,有一个异常接口,该接口所有实现结构体就是异常对象
  • go中有一个很有意思的final实现方式,就是标识语句为 defer这样这条语句就是一个final内的语句,无论是否会发生异常都会被执行,另外defer语句是逆序执行,极大的方便了代码的编写,但是遗憾的是final同样不能指定域,只能是函数/方法的域,因此一旦进入final就会进入退出函数的流程;
  • go中不鼓励使用类似java的try-catch捕获异常,但是提供了这套机制(但是有时候可以极大的简化开发)panic相当于throw、recover:相当于catch、不过遗憾的是go中没有try-catch、这就使得一旦panic抛出异常将会直接进入到达defer(即final)、为了实现catch需在其中设置recover中进行异常捕获,由于defer的设置,所以使用go的异常抛出时需要谨慎,因为这意味着下面的逻辑代码将不再被执行,而值返回的形式就可以逻辑判断再处理
func printHello() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	//do假设这个函数抛出panic异常,就会被recover捕获;显然如果异常不处理就会终止程序
}

附录

  1. 格式化输出表(Printf)

    image-20220525173954968
  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机制【defer func】可以说是go的一个亮点,并且go优化后的defer实现,使得defer几乎没有多大的成本;前面大概介绍了defer作用就是相当于final,而且是FIFO的
  • defer必须后面跟着func,不能是普通语句的主要原因就是:defer实现是通过goroutine的栈机制实现的
    • defer通过一个结构体_defer保存执行需要的信息
      • 参数和结果信息
      • 栈帧信息
      • 函数保存位置下一个defer的指针
    • 每次将其保存在goroutine的defer调用链的首部
    • defer结构体可以通过三种方式分配:栈上分配、堆中分配、开放编码
  • 特点
    • FIFO
    • 只能是函数,所以函数传参的话也会遵守值传递的规则
    • 允许函数闭包

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的强大特性并不是体现在前面的简介的代码上,而是在并发编程上

协程

img
  • java的Executor框架(实现线程池)并不是协程主要原因是其调度器、但是Fork\Join框架和协程实现非常类似,只不过其由于还是使用进程作为调度的单位,更为重量级,下面参考Java的Fork/Jon学习协程
  • 轻量级
    • go中没有对象的概念,这使得go可以很简单的开启协程:go 函数,并且使用的是方法,这使得协程非常轻量级
    • 在go中协程是两级调度模型,main函数在main线程中以主协程的形式进行,go开启的其他协程都是以从协程进行
    • go的sync包类似java的juc包,但是其实现了调度器的概念;
  • go中协程之间的通信和Java线程通信理念不同,在java中都是通过共享内存实现,在go中是通过通信实现(在Java中天生存在堆,只要持有堆引用即可实现线程将通信(无论是static、InitThreadLocal、还是实例变量传递或者volatile都是这种思想))在go中使用channel作为通信通道进行通信,另外为了实现同步也提供了同步机制;
  • channel支持通过select支持多路复用的方式,在go中select的使用方式(语法糖)和switch语法接近,都是select - case - default机制
    • 没有default时则为阻塞等待,必须等到有一个通道可以读/写
    • 默认就有break,所以选择一个case后就不会继续选择case/default
    • 如果有default,那么每次轮询都会有选择(当没有case选择时就会选择default)
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)
}

其他

单元测试

  • go中没有提供测试框架+注解,所以通过测试框架+命名的方式进行单元测试;
    • XXX_test + testing + go test:go的单元测试框架
  • 使用步骤
    • 创建XXX_test
    • 在test中创建func:testXxxxx
//假设这是一个abc_test
func testName(t *testing.T) {
    //测试的函数
    res := name()
    if res== `a` {
        t.Fatalf("执行错误")
    } 
    t.Logs("执行成功")
}

//在文件目录执行go test

反射

  • 由于go没有类似Java的Class这种入口,在go中反射是通过每个interface持有的(value,type)对实现的

  • type是一个接口,有17种实现类(即17个type类型)

    image-20220614232551899
  • value

    • value保存了type外,还有一个unsafe.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

基础
在这里插入图片描述
  • 在go中socket编程省略了bind,服务器再listen就会直接执行bind+listen两步操作

  • socket编程主要:原生socket连接操作Conn的后处理操作

    • socket操作:Dail、Listen、Accept

    • Conn操作:

      • 主要是Read、Write、Close

      • 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
        }
        
心跳机制
  • 在TCP中,本身具有心跳机制,通过心跳包维持TCP连接;当然go也是支持SetKeepLive设置心跳的;
  • 但是很多时候,我们可能会自己实现一套心跳机制来控制我们关闭逻辑;这时利用TCP的goroutine和channel即可快速实现

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包

基础

  • unsafe不保证向后兼容
  • 由于go不支持直接操作指针,所以提供一种机制,让其可以像C语言一种直接操作内存地址
  • unsafe可以通过Pointer的机制强制类型转换(跳过go安全检查)

Pointer

  • Pointer是unsafe的自定义类型

    • type Pointer *ArbitraryType该类型底层就是一个ArbitraryType类型的地址任意类型地址
  • Pointer 表示指向任意类型的指针。 Pointer 类型有四种特殊操作可用:

    • 任何类型的指针值都可以转换为 Pointer。
    • Pointer指针可以转换为任何类型的指针值。
    • uintptr 可以转换为指针。
    • 指针可以转换为 uintptr。
  • 因此,指针允许程序破坏类型系统并读写任意内存。使用时应格外小心。以下涉及 Pointer 的模式是有效的。不使用这些模式的代码很可能在今天无效或在未来变得无效。

    • (1) 将 T1 转换为指向 T2 的指针。假设 **T2 不大于 T1 并且两者共享相同的内存布局,则此转换允许将一种类型的数据重新解释为另一种类型的数据。**一个例子是 math.Float64bits 的实现:

      func Float64bits(f float64) uint64 { 
          return (uint64)(unsafe.Pointer(&f)) 
      }
      
    • (2) 将指针转换为 uintptr(但不返回指针):将指针转换为 uintptr 会生成指向值的内存地址,作为整数。这个时候:

      • 将 uintptr 转换回 Pointer 通常是无效的。(垃圾回收和对象移动)
      • uintptr 是整数,而不是引用。
      • 将指针转换为 uintptr 会创建一个没有指针语义的整数值。
      • 即使 uintptr 拥有某个对象的地址,如果对象移动,垃圾收集器也不会更新该 uintptr 的值,该 uintptr 也不会阻止对象被回收。
    • (3)允许使用地址运算将指针转换为 uintptr 然后通过Pointer获取地址。(例子如下),但是

      • 运算只能在一个表达式中;

      • 不允许通过对nil进行地址运算;

      • 不允许通过地址运算获取分配的末尾;

        例子

      • // 结构体地址运算 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)
        
    • (4) 调用 syscall.Syscall 时将指针转换为 uintptr。(还未学习,略)

    • (5) 反射包下的地址/地址数值转换运算。尽管是后兼任的,但是也要符合unsafe包的地址运算规则;

    • (6) reflect.SliceHeader 或 reflect.StringHeader 数据字段与指针的转换。(见下文反射实现)

API

image-20220616164349684

  • 自定义类型,在unsafe中为了方便时使用自定义了两种类型:ArbitraryType IntegerType ,底层都是int;
    • type ArbitraryType int任意类型
    • type IntegerType int任意整数类型
  • 五种方法
    • Sizeof:变量占用的内存大小
    • Offsetof获取结构体/切片/数组中某个元素的偏移量
    • Aligonf获取对齐值
    • Add地址运算,等价于unsafe.Pointer(uintptr(unsafe.Pointer(&xxxxx)) + unsafe.Offsetof(XXXXX))
    • Slice获取一个切片,其底层数组从 ptr 开始,长度和容量为 len。

补充

  • 前面说到string/切片的实现说到unsafeheader包,这个包尽管不再unsafebao,但是也是一个unsafe操作的包;这里涉及到切片和string的底层实现
string和slice实现
  • 本质都是通过unsafe.Pointer保存数据的地址,通过Len控制访问的空间(可以偏移的大小)
  • Slice通过Cap记录申请到的空间的大小,决定是否需要再次申请空间;
type Slice struct {
	Data unsafe.Pointer
	Len  int
	Cap  int
}

type String struct {
	Data unsafe.Pointer
	Len  int
}

工具类型包

time包

  • time包时go的标椎时间包

数据解析类型包

并发包

  • sync:同步机制
  • sync/atomic原子变量
  • context:上下文
  • channel协程通信

sync包

  • 其实现和java的JUC包思路很相似
  • 主要提供的实现有
    • Mutex:互斥锁,不可重入
    • RWMutex:读写锁
    • WaitGroup:简易的Fork/Join,通过wait控制当等待数量大于0是阻塞
    • Cond:相当于JUC的condition,使用方法也差不多
    • Once:这个非常特殊,Once标记的函数将只被执行一次
    • Poll对象池,在go中为了尽可能的减少GC,除了极可能栈上分配外就是对象池技术

sync/atomic

基础
  • atomic操作进行无锁更新;
    • 和JAVA不同,不需要使用原子变量,只需要使用原子操作函数即可使用对应类型变量的原子操作
  • Value:相当于原子变量,可以装载any(任意类型)的变量;
    • 不能存储nil(存nil会抛出panic);
    • value中存储的第一个值,决定了其后续的值类型(以后只能存储此类型的值);
    • 尝试存储不同的类型,会抛出panic;
原子无锁操作
  • atomic无锁操作支持:int32、int64、uint32、uint64、uintptr、unsafe.Pointer,六种数据类型
  • 主要的无锁原子操作有:swap、cas、Add、Load和Store五种类型
  • API如下
image-20220616163305282 image-20220616163339538
Value
  • Value是扩展的(类似java原子变量)的数据结构,提供了四种原子无锁操作方法
    • image-20220616163942491

context

特点
  • context 用来解决 goroutine 之间退出通知元数据传递的功能。
    • 由于在go中,我们不能直接杀死协程;
    • 协程将没有归属关系(本质没有在于没有父协程、子协程),可以通过context使得他们具有关系(Context将可以具有父子关系),而且context线程安全(并发安全),所以可以将一个context传入多个goroutine
  • channel+select机制控制本质不是用来控制协程关系的(是一个协程的通信机制);
使用
classes
  • 不要将 Context 存放在结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx
  • 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context.todo
  • 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  • 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的
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): 
            
        }
    }
}
实现
  • 接口
    • Context:上下文接口,主要包含四个方法:ValueDoneDeadlineErr
      • Value获取 key 对应的 value,可以通过Context在goroutine将传递数据
      • Done:当 context 被取消或者到了 deadline,返回一个被关闭的 channel
      • Deadline:返回 context 是否会被取消以及自动取消时间
      • Err:在 channel Done 关闭后,返回 context 取消原因
    • canceler:取消方法接口
      • cancelcancel方法通知后续创建的goroutine退出
      • Done返回chan,后端goroutine可以通过监听确定是否需要退出
  • 结构体(四个默认实现)
    • emptyCtx空的Context一般用于占位或者标记为根context,下面两种方法生成改类型context
      • Background:一般根的Context可以通过这种方法生成
      • TODO:占位,当前没有context需要传递到函数中,但是 不能传递nil,所以使用TODO生成占位的Context
    • cancelCtx可以取消的context
      • WithCancel
    • timerCtx:定时取消的context
      • WithTimeout
    • valueCtx可以携带数据的context,数据通过key value键值对形式传入
      • WithValue推荐使用结构体/接口类型的value,而且key必须是可以比较的

实现

  • valueCtxtimerCtx&cancelCtx

    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
    }
    
  • 对于context包,整体代码非常简介,下面主要学习两个生成context的方法

    1. WithTimeout

      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实现倒叙排序、结构体排序
  • 提供了排序、二分查找(查找不到返回插入位置)、

    image-20220528204738680

encoding

  • json序列化:Marshal方法:序列化;Unmarshal:反序列化

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

系统类型包

io和os

  • 文件操作:和Java的基本一样,通过一个File操作文件

    • 打开方式(和C语言差不多)

      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  // 如果可能,打开时清空文件
      )
      
    • image-20220528212520311
    • Reader

      image-20220528214139065
    • io工具包

      image-20220528213913545

实现

  • 下面将根据《go语言设计与实现》大概学习一下基本实现思想(对比Java),在并发编程中再根据源码深入学习

编译

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

编译的过程

AST
  • 和Java差不多,都是解析生成一棵语法树
  • 首先先通过词法分析获取获取TOKEN序列
  • 将TOKEN序列进行语法分析解析成语法树,这个过程只涉及语法检查;
中间代码
生成
  • 这一过程和Java的类加载机制类似,目标都是让后续可以直接读取生成可自行的机器码
    • go中没有双亲委派机制的说法(因为编译成语法树本身已经完成了这部分工作)
      • go中同样会需要导入外部依赖
    • go和Java一样有连接的过程
      • 校验:检查变量、类型、函数和其类型
      • 准备:为变量赋初值
      • 初始化:变量初始化
  • SSA特性:go的中间代码具有SSA特性(即静态单赋值),话句话说就是对一个变量两次定义会使得第一次失效;
  • runtime包替换
    • 函数替换:go通过代码替换将make、new、select等替换为runtime包下真正实现该功能的函数
      • 例如channel,就是chan文件的makechan
    • 语法糖实现:go为了简化开发,使用了大量语法糖(例如channel),go会将其中语法糖替换为真正的函数以实现其功能
      • 例如channel就是将<-替换为runtime下chan下的相应函数
  • 生成SSA代码
  • 经过多次的SSA编译(并发编译)就生成了中间代码
img

运行

  • 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一样,本质就是将线程状态在用户态模拟实现,所以主要有以下状态:可执行、执行、阻塞(等待)、死亡

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

  • go中创建goroutine过程:

    • 获取或创建goroutine对象(go中内置了协程池以实现复用goroutine)
    • 通过传入参数初始化goroutine上栈保存的数据(局部变量表、退出信息、引用信息等)
    • 设置goroutine参数(设置goroutine一系列属性:调度信息、栈信息、计数器信息等)
    • 加入运行队列(局部/全局)
M
  • 本质就是一个循环不断执行的线程,M被创建出来后就会向Executor的worker一样不断的处理goroutine;
  • go的调度器中有两级调度队列,所以在查找调度的goroutine的时候会有特别的处理点
    • 需要尽可能保证公平
    • 最大化利用CPU利用率
      • 两级调度队列获取:之所以设计两级调度队列是因为:当前线程的局部队列调度是不需要加锁的、调度全局队列都是需要加锁的,go为了优化所以使用了两级调度
      • 窃取:为了尽可能让goroutine整体比较均衡,允许窃取其他线程的局部调度队列的goroutine
P
  • 调度器处理器和AQS的最大区别除了调度功能就是全局、局部调度队列的概念、允许窃取非当前线程的其他线程的局部调度队列(运行队列)的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
  • 对分配的内存块做好标记,这小块内存中,哪些位置是指针,我们用一个 bitmap 对应记录下来,本质就是非常像JVM的RememberSet

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舔猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值