• 面经 | Go语言知识点



    GoLang
    golang 基础 map slice interface 和并发等基本原理搞清楚,gmp goorutine 基本原理,以及垃圾回收机制等

    Go其实是一个不严格是面向什么的语言,和传统的面向对象变成是有区别的。
    go没有类的定义,但是结构体在使用上其实和类差不多,可以通过struct实现OOP特性。
    去除了传统OOP的方法重载、构造函数和析构函数,隐藏了this指针
    但是仍然具有面向对象的继承、封装和多态的特性,只不过实现传统面向对象的不一样,如继承不用extends关键字而是使用匿名字段实现。
    Go语言严格区分大小写,入口执行函数是main(),不需要手动在语句末尾添加分号,编译器会自动添加。
    是按照一行进行编译的,所以一行只有一个语句。如果有没有用的变量和包,是会报错的。

    基础

    声明变量可以不用赋值
    赋值

    var a int=1
    //等价于
    a:=1
    //数组定义
    var x [2]int
    var array=[5]int{1,2,3,4,5}
    array:=[5]int{1,2,3,4,5}
    //数组使用[...]表示不确定长度初始化
    array:=[...]float32{10.0,2.0,3.4,50.0}
    //数组可以调用append的方式添加其中
    animals:=[][]string{}
    row:=[]string{"bird"}
    animals=append(animals,row1)
    //等价于
    x:=1
    
    //静态常亮,会自动判断对象类型
    const s string = "123"
    const s = "123"
    //可以表示枚举类型
    const{
        one = 1
        two = 2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    有一个iota,这个是可以让编译器修改这个值,iota在const出现的时候被重置为0,每执行一行const就加1

    const{
        a=iota
        b
        c
    }
    //输出是:0,1,2。因为默认值为0,执行一行iota会加1
    const (
        a = iota   //0
        b          //1
        c          //2
        d = "ha"   //独立值,iota += 1
        e          //"ha"   iota += 1
        f = 100    //iota +=1
        g          //100  iota +=1
        h = iota   //7,恢复计数
        i          //8
    )
    //输出是:0 1 2 ha ha 100 100 7 8
    const (
        i=1<<iota
        j=3<<iota
        k
        l
    )
    
    func main() {
        fmt.Println("i=",i)
        fmt.Println("j=",j)
        fmt.Println("k=",k)
        fmt.Println("l=",l)
    }
    //输出是:1,6,12,24.就是如果这一行不写,就会执行上一行的命令,然后iota再+1,就是3<<2和3<<3 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    输出
    只有fmt.Printf是格式化输出,使用%v输出变量的值value,如果是%+v则会以k-v形式打印出变量,比如对于结构体

    //引入包
    import "fmt"
    fmt.Print("xxx")、fmt.Print("xxx")
    fmt.Printf("xxx")
    prin和pting("xxx") println("xxx")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    条件语句和循环语句

    if不用说了,差不多,条件不带括号,代码块带括号
    switch语句
    这个还不太一样,C语言的switch有break,不然会走所有case。这个不需要break也不会走所有case,只会走当前case
    有特殊用法,可以判断interface接口中实际存储的变量类型

    var x interface{}
    switch i := x.(type) {
    case nil:
    	println("是nil")
    case *int:
    	println("*int", i)
    case int:
    	println("这是int")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    虽然不需要用break控制他不继续往下走case,但是可以使用fallthrought控制他继续往下走case
    循环语句
    for循环

    //假设animals是一个二维数组
    for i:=range animals{
        fmt.Print(snimals[i])
    }
    
    • 1
    • 2
    • 3
    • 4

    然后,虽然不需要使用break,但是在for循环之类的还是可以用break跳出循环之类的结构,而且可以通过标记指示break需要跳出哪个代码块。
    如下,就会跳出re包含的代码块

    re:
        for i:=0;i<10;i++{
            break re
        }
    
    • 1
    • 2
    • 3
    • 4

    对于continue也可以通过标记指定需要从哪个代码块开始重新执行。
    go里面是有goto这个关键词的,可以实现条件转移、跳转循环之类的,但是也不推荐使用,和C语言一样

    LOOP:
        xxx
    goto LOOP
    yyy
    
    • 1
    • 2
    • 3
    • 4
    函数

    函数的定义如下

    func {name}([parameters]) [return type]{
        ...
    }
    //示例
    func test(a int) int {
    	return a + 1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    返回值可以返回多个返回值

    func swap(x, y string) (string, string) {
       return y, x
    }
    
    • 1
    • 2
    • 3

    有几个内置函数

    • len():返回数组或者切片长度
    • cap():计算容量,切片的最大长度可以到多少
    • uncafe.Sizeof()

    参数
    参数类型包括值类型和引用类型
    值类型包括:int、float、bool和string,其变量直接指向内存中的具体值,当复制的时候其实就是拷贝的过程。可以使用&获取到变量地址,和C语言其实一样。
    值传递的参数在函数中使用是通过拷贝的方式,形参不会修改实参的值
    引用的方式就会改变实参的值,就是指针的方式

    func swap(a *int, b *int) {
    	var temp int
    	temp = *a
    	*a = *b
    	*b = temp
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    也可以在函数内部创建一个函数

    func main(){
       /* 声明函数变量 */
       getSquareRoot := func(x float64) float64 {
          return math.Sqrt(x)
       }
    
       /* 使用函数 */
       fmt.Println(getSquareRoot(9))
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    go也有闭包,闭包的优势就在于可以直接使用函数的内部变量不需要进行声明
    就是外部函数的返回值是一个内部函数,直接在return写内部函数体

    切片

    两种声明方式
    make和声明空切片

    //声明一个没有指定大小的数组就可以定义切片,容量长度为0,可以通过append添加元素
    var x []int
    x:=[]int{}
    //使用make()创建切片,指定长度为10
    var slice []int=make([int],10)
    //声明初始化,简写
    slice := make([]int, 3)
    slice = []int{1, 2, 3}
    
    //添加元素,返回值是原切片,第一个参数是原切片,第二个是插入的
    slice=append(slice,2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    使用的话比如截取之类的,就类似python的list使用方法,中括号和冒号
    扩容
    切片的默认的capacity是0,然后如上给他初始化{1,2,3}以后,长度和容量变为3。
    之后如果删除元素,容量不变,长度减少
    添加元素,长度增加,如果大于容量值了,会进行扩容

    • 扩容后元素长度小于1024,会扩容为原长度的2倍
    • 扩容后元素长度大于1024,会扩容为原长度的1.25倍
    • 如果扩容的新容量大于原有的二倍,直接扩容到新容量

    然后不需要扩容的时候append返回的是原底层数组的原切片,扩容以后的话返回的是新底层数组的新切片,地址会变。
    扩容会调用growslice()函数,这个函数最后两行有一个是capmem和newcap,后者会对内存进行一个对齐,具体和内存分配策略有关,所以结果不一定是1.25的整数倍。
    go是按照size class大小分配的,有一个数组runtime/sizeclasses.class_to_size,在其中找到一块最小的满足要求的内存分配

    var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
    
    • 1

    比如一个切片里面有五个int,只需要40字节,但是会找到一块48字节的内存给他用。
    截取
    可以用一个切片中截取部分数据赋值给新的切片,用法就类似于python的listx:=slize[1:3]
    截取就是new一个新的slice,底层数组服用原先的。长度是截取数据的个数,容量是截取开始下标到原切片的结尾。

    slice:=[int]{1,2,3,4,5,6}
    s1:=s[1:3]
    //s1的内容为2和3,长度为2,容量为2,因为是从开始下标算起,从1开始到末尾,
    
    • 1
    • 2
    • 3

    使用copy(sc,s)可以实现将s切片拷贝给sc切片。拷贝数据的个数是min(len(s),len(sc)),因此需要两个数组大小一致才能完全拷贝

    底层结构

    一个切片结构由三部分组成:指针、长度和容量。指针指向底层数组,长度就是当前长度,容量是底层数组的长度。
    切片只是一个只读对象,相当于是对数组指针的一个封装。
    Go的值传递,用切片传递数组参数既可以节省内存也可以更好处理共享内存问题。
    但是并不是什么时候都适合用切片代替数组,因为切片底层数组可能会在堆分配内存,而且小数组在栈上拷贝的消耗未必比make消耗大。

    集合Map

    声明一个map,可以直接map也可以使用make创建

    var map_map map[ke_type]value_type
    var map_map=make(map[ke_type]value_type)
    
    • 1
    • 2

    然后赋值的时候就是普通的字典使用方式test_map[“111”]=“222”
    然后有一点好的是,在根据键获取值的时候,可以有两个返回值,第一个是值,第二个是一个布尔型,如果存在,就返回值和true。否则返回空字符串和和false
    delete()函数可以删除集合中的元素,根据键

    底层结构

    GO中map的底层实现
    没有总结完
    底层是一个hash表,通过键值进行映射。
    buckets中存放了哈希中的最小粒度单元bucket桶,数据通过hash函数均匀分布在每个bucket中,其存放的是bucket的地址
    bucket
    每个bucket最多存放八组key-value,多余的会放在下一个bmap里面,使用overflow指向,这些数据都不是显式保存的,通过偏移量计算得到的。

    • tophash存储哈希函数算出的高八位,加快索引的,只需要比较高八位就可以过滤掉不符合的,然后再比较完整哈希得到value
    • 存储的是key-value,底层将key和value分别放在一起,key大于128字节的时候会用指针存储,value一样。避免长度不同带来的问题
    • 当bucket溢出的时候,指针指向下一个bucket overflow

    查找
    用高八位和第八位哈希进行定位查询,高八位查询bucket是否有对应值(在bucket中的位置),低八位确定数据在哪个bucket。

    • 根据key计算哈希
    • 取哈希确定bucket位置
    • 查询tophash数组,如果tophash[i]等于高八位,就在这个bucket里面寻找
    • 如果没有,顺着往下找

    如果找不到返回对应类型的0值
    插入元素

    • 计算key的哈希
    • 直接适用低八位哈希确定位置
    • 判断key是否存在,存在更新
    • 否则插入
    扩容

    负载因子
    键值对的数据与桶的数目的比值叫负载因子。
    触发扩容的两种方式

    • 负载因子>6/5:平均每个bucket存储6.5个键值对
    • 桶溢出的时候:如果B<15,最大overflow的bucket是2B。如果B>=15,overflow的bucket数量为215的时候

    渐进式扩容
    存储较多的键值的是哦胡,一次迁移需要成本太大了,因此先分配够多的桶,然后使用一个旧的字段记录旧同位置,一个字段记录迁移进去,相当于偏移量。如果当前是扩容阶段,完成一部份键值对任务的迁移。
    将键值对的迁移分成多次进行,避免一次的扩容带来抖动。
    等量扩容
    创建和旧的一样多的bucket,把原来的复制进去。
    为什么呢负载因子没满bucket满了呢,因为有很多键值被删了的情况。导致中间可能会出现空位,导师会溢出很多,这个实际上是一种整理。元素在bucket内会重排,不会换桶
    增量扩容
    当前bucket不够的时候,扩容,元素会重排,可能会发生桶偏移。
    负载因子过大的时候就考虑一个新bucket,新的长度是原有的两倍,旧数据弄到新bucket求。如果一次完整搬迁可能会很耗时间,因此用逐步搬迁,每次访问map都会触发一次,每次搬两个键值对。

    接口

    Go语言基础 - 接口
    接口是一种数据类型,可以把具有共性的方法定义在一起,其他类型只需要实现这个接口就可以实现其中的方法。

    大概的用法感觉主要就是体现在函数传参之类的,如果有多个结构体,如果有一个函数需要将这些结构体全部作为参数,那么需要好多个函数,因为参数类型不一样。但是如果用了接口,就只需要一个函数,参数类型是接口类型,然后所有实现接口的所有方法的结构体都可以作为参数进来。

    大致步骤:

    • 声明一个接口

    • 定义一个结构体

    • 给结构体绑定所有的接口方法

    • 使用,在函数参数中将类型写成接口即可

    代码

    type Phone interface {
        buy()
    }
    
    type Apple struct {
        Price int
        id    string
    }
    //函数
    func (Apple Apple) buy() {
        fmt.Println("买了一个手i及")
    }
    func use(MyPhone Phone) {
        MyPhone.buy()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    参数的传递可以是值传递也是引用传递。引用传递就是传一个指针进去,可以直接修改对象信息
    java里面的接口是显式声明,go里面的叫隐式声明,只要一个类型实现了接口中规定的方法,那么就实现了这个接口。
    然后只要多个类实现了一个接口,其中一个类型的变量就可以直接赋值为另外几个类型的变量。
    然后其实可以实现多个接口,只需要给结构体分别绑定所有方法即可。
    如果是空接口interface{},作为函数参数的话,表明可以接受任意类型的参数;如果作为map的value类型,那么value可以储存任意类型

    test_map_2 := make(map[string]interface{})
    test_map_2["1"] = "1"
    test_map_2["2"] = 2
    test_map_2["3"] = false
    
    fmt.Println("测试空接口作为map的value类型")
    for k, v := range test_map_2 {
    	fmt.Println(k, v)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接口值
    因为接口的值可以是任意一个实现该接口的类型变量值,因此接口除了记录值以外还需要记录这个值的类型,因此由两部分组成,type和value
    如下两种,第一种value是Dog的值Name,type则是*Dog类型;第二种其动态值则为nil,而类型仍然为*Dog

    type Mover interface{
        move()
    }
    //Dog是实现接口的结构体
    var m Mover
    m=&Dog{Name:"123"}
    m=new(Dog)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    接口变量(如m)之间是可以比较的

    • 如上第二种情况虽然动态value时nil,但是因为类型时*Dog,所以比较的时候不等于nil
    • 两个接口变量进行比较的时候,会比较其动态类型type和动态值value
    • 当type包含不支持互相比较的如切片,比较的时候就会报错

    类型断言
    接口值可能为任意类型
    可以使用fmt包直接打印出类型
    fmt是使用反射机制在程序运行时获得动态类型的名称的

    fmt.Printf("%T\n",m)
    
    • 1

    可以使用x.(type)获得对应实际值的类型。
    返回两个参数,第一个是x转化为T后的变量,第二个是布尔值,断言是否成功

    x.(T)
    
    var n Mover = &Dog{Name: "旺财"}
    v, ok := n.(*Dog)
    if ok {
    	fmt.Println("类型断言成功")
       	v.Name = "富贵" // 变量v是*Dog类型
    } else {
      	fmt.Println("类型断言失败")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如果有多个可以使用switch判断。

    switch x.(type){
        case int:xxx
        case bool:yyy
        default:zzz
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    底层实现

    是一种特殊类型,存在两种接口,一种是带有方法的接口,一种是不带方法的接口。所有变量都可以赋值给空的接口变量,实现接口中定义的方法的变量就可以赋值给带方法的接口变量,并且可以通过接口直接调用对应方法,实现多态。
    内部定义
    没有方法的接口在内部定义为eface结构体;有方法的接口内部定义为iface结构体。
    两种类型都是定义为两个字段的结构体,大小都是16字节。
    Go语言中有一个_type类型结构体,记录某些数据类型的一些基本特征,比如占用的内存大小,类型名称等。每个类型都有一个与之对应的结构体,如果比较特殊的类型,可以对其扩展,如接口的类型结构体就是interfacetype,包含有额外字段。
    看不懂底层实现

    Context

    # Go context详解

    包含有goroutine的运行状态、环境、现场等信息

    主要用于在goutine之间传递上下文信息,包含有:取消信号、超时时间、截至时间、k-v等

    context.Context类型的值可以协调多个goroutine中的代码进行取消操作,并且可以存储键值对。是并发安全的,比如进行取消HTTP请求的操作。

    起因

    Go经常用于编写高并发的后台服务,搭建http server,通常一个请求里可以请求若干个goroutine,有的进行数据库操作,有的调用接口。他们之间需要共享同一个请求的一些基本信息,比如登录的token,处理请求的最大超时时间等,如果到达超时,需要关闭处理的goroutine,资源会说。

    go的server模型其实是协程模型,一个协程处理一个请求。

    一个场景

    在业务高峰期的时候,有的服务响应变慢,没有超时机制,等待服务的协程越来越多,资源消耗逐渐增大,然后可能就会出意外。这给情况只要设置一个超时时间就行了,如果超时还没有得到数据,就返回默认值或者报错。

    Go中不能直接杀死协程,一般通过channel+select进行控制。但是有时候一个请求有多个协程处理,需要共享一些全局变量等等,而且需要被同时关闭,就可以用context上下文控制。

    context用于解决goroutine之间的退出通知和原数据传递的问题

    底层原理

    看链接吧

    然后
    Context接口主要由四个方法

    type Context interface {
        // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
        Done() <-chan struct{}
    
        // 在 channel Done 关闭后,返回 context 取消原因
        Err() error
    
        // 返回 context 是否会被取消以及自动取消时间(即 deadline)
        Deadline() (deadline time.Time, ok bool)
    
        // 获取 key 对应的 value
        Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • Done():返回一个channel,可以表示context被取消的信号。当channel被关闭时,说明context被取消了。这是一个只读的channel,而又知道读取一个关闭的channel会读取到对应0值,且没有其他地方给这里插入元素。因此除非被关闭否则读不出数据,即当读出0的时候,可以认为是需要关闭了。
    • Err():返回channel关闭的原因,被取消还是超时
    • Deadline():返回context截止时间,可以判断接下来干啥。
    • Value():获取之前设置的key对应的value

    cancel()函数
    功能就是关闭channel:c.down(),递归取消所有子节点,从父结点删除自己。

    使用

    如下是个简单用法,给参数传入一个k-v

    func main() {
        ctx := context.Background()
        process(ctx)
    
        ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
        process(ctx)
    }
    
    func process(ctx context.Context) {
        traceId, ok := ctx.Value("traceId").(string)
        if ok {
            fmt.Printf("process over. trace_id=%s\n", traceId)
        } else {
            fmt.Printf("process over. no trace_id\n")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    取消goroutine
    场景:启动一个功能定期向后端轮询,后台启动一个协程,然后客户端关闭功能,需要退出goroutine
    普通的方法可能会在goroutine的循环里加一个flag,通过true和false判断是否继续。
    如果goroutine多了,循环嵌套多了就不好了。
    使用context处理,在go的函数里使用select进行判断一下。context本身是没有取消函数的,是保证只能从外部函数调用取消函数,避免子节点调用。

    func Perform(ctx context.Context) {
        for {
            calculatePos()
            sendResult()
    
            select {
            case <-ctx.Done():
                // 被取消,直接返回
                return
            case <-time.After(time.Second):
                // block 1 秒钟 
            }
        }
    } 
    ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
    go Perform(ctx)
    
    // ……
    // app 端返回页面,调用cancel 函数
    cancel()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    并发

    Go最大的特点就是从语言层面支持并发,不用关心底层逻辑、内存管理,只要编写好自己的业务逻辑。也自带一个比较好的垃圾回收机制,不用关心线程的创建和销毁。
    通过go关键词可以开启goroutine,这个是一种轻量级线程,有自己的栈和程序计数器等,由GoLang负责调度。
    同一个程序中的所有go routine共享一个地址空间
    并发是基于CSP(通信顺序进程)的,这个模型是描述两个独立并发实体通过共享管道(channel)进行通信的模型。
    go关键字其实就是协程,实现了goroutine的内存共享,比线程更加好用高效
    协程和线程的区别
    最重要的区别就是线程切换需要进入内核态,进行上下文的切换,而协程在用户态就可以进行切换,开销小。
    用户可以自行决定何时切换协程
    线程固有的栈大小是2M,用于保存局部变量之类的信息;而goroutine里固定大小的栈可能会资源浪费,使用动态扩张收缩策略,初始化2KB,最大可以到1GB

    go RuntineTask("go")
    	RuntineTask("普通")
    	time.Sleep(time.Second * 1)
    
    }
    
    func RuntineTask(Message string) {
    	for i := 0; i < 10; i++ {
    		fmt.Println(Message)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里一定要给一个延时感觉,不然就go来不及执行协程他就结束main了
    不对,也可以通过一个计数器实现sync.WaitGroup
    协程终止会调用defer函数,这个函数里执行defer 函数,函数里调用wg.down表明计数器减1
    当计数器0-的时候程序结束了,不然还没执行程序就结束了。

    var wg  sync.WaitGroup
    	wg.Add(2)
    	go func() {
    		defer wg.Done()
    		for i:=1; i<100;i++  {
    			fmt.Println("A:",i)
    		}
    	}()
    	go func() {
    		defer wg.Done()
    		for i:=1; i<100;i++  {
    			fmt.Println("B:",i)
    		}
    	}()
    	wg.Wait()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    channel
    channel是连接协程之间的一个机制,可以让一个协程发送特定值到另一个协程。
    go语言中channel是一个特殊的类型,相当于一个传送带或者队列,遵循先入先出的原则,保证收发顺序,声明一个channel的时候需要指定数据类型。
    使用-<操作符用于指定通道的方向,如发送还是接收

    //声明一个通道
    ch := make(chan int)
    v:=3
    //传递数据
    ch-<v
    v:=-<ch
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    默认情况下channel是没有缓冲区的,是阻塞式,两端必须同时有发送和接收才行。
    也可以在初始化的时候指定一个大小作为缓冲区,这样就可以异步了,当存储的数据超过缓冲区时,就会停止插入数据。发送方会阻塞到值被拷贝到缓冲区,然后缓冲区满了以后,就会阻塞到直到缓冲区有空余
    一个小例子

    func testchannel(Num int, ch chan int) {
    	ch <- (Num * Num)
    }
    func main(){
        ch := make(chan int)
    	go testchannel(4, ch)
    	go testchannel(5, ch)
    	aa := <-ch
    	bb := <-ch
    	fmt.Println(aa, bb)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    也可以使用range遍历读取的数据。
    通过close(cn)可以关闭通道。

    调度器

    CPU感知不到goroutine的,只知道内核线程,M就是对于内核线程的一种封装,Go调度本质就是将G分配给M执行。
    设计思想

    • 线程复用
    • 并行(利用多核CPU)
    • 抢占调度(解决公平)

    Go的runtine实现了一个小型任务调度器,可以将CPU资源分给每个任务,主要三个函数
    Gosched()
    使当前Go协程放弃处理器,让其他协程运行,不会挂起协程,未来会恢复运行。Go的协程是抢占式的,长时间执行或者系统调用的时候会把CPU释放,让其他runtine运行。
    出现这几种情况就会发生调度

    • syscall
    • 调用C语言函数
    • 调用Gosched
    • goruntine运行超过10ms(可以被抢占)而且调用了非内联函数

    Goexit():
    终止调用的Go协程,其他协程不影响,在终止之前会执行所有defer函数。
    go协程需要等主函数执行完再退出,如果主线程停了好像就不运行了,需要在main加个Sleep或者用WorkGroup

    func RuntineTask1() {
    	defer fmt.Println("这是task  1的defer")
    	fmt.Println("这是task2  的普通函数")
    }
    func RuntineTask2() {
    	defer fmt.Println("这是task  2的defer")
    	fmt.Println("这是 的普通task2")
    
    go RuntineTask1()
    go RuntineTask2()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    GOMAXPROCS
    设置程序运行时使用的CPU数,默认使用最大CPU计算。设置的同时可以返回可执行的最大CPU数。
    通过runtime.GOMAXPROCS(1)可以设置使用的核心数。

    GMP

    GMP是Go运行时调度层面的实现,包括G、M、P和Sched
    G
    代表Goroutine协程,存储执行栈信息、状态和任务函数等,G的数量是无限制的,受内存限制。一个G的栈大小2-4K,一般简单的机器也可以启动数十万个goroutine。G退出的时候还可以把G清理的内存放在P或者全局闲置列表gFree复用。
    M
    代表Machine,Go对操作系统线程的一个封装,相当于操作系统内核线程,现在CPU执行代码必须有线程,通过系统调用clone创建线程。M在绑定一个有效的P以后进入一个调度循环。
    循环的机制大概就是从P的本地队列以及全局队列中获得G,切换到G的执行栈上并运行G的函数,调用goexit清理工作回到M。M不会保留G的状态,这是G可以跨M调度的基础。
    M的数量有限,默认一千,可以设置最大数。
    P
    虚拟处理器Processor,M执行G所需要的资源和上下文,只有将M和P绑定,才能让P的runq中的G真正运行起来,P的数量决定了系统最大可并行的G的数量,P的数量受到本机CPU的限制,也可以通过变量修改,默认CPU核心数。
    Sched
    调度器结构,维护有存储的M和G的全局队列,以及调度器的一些状态信息。
    但是这个模型的缺点是不支持抢占式调度。如果一个G有死循环逻辑,G将永久占用分配的M和P,这个M和P的其他G会被饿死。
    因此后来引进了基于写作的抢占式调度和基于信号的抢占式调度。

    内存管理

    # go——内存分配机制
    Go可以自主管理内存,包括如内存池、预分配等机制,不会每次分配内存都进行系统调用。
    设计思想

    • 每个线程维护自己的独立内存池,分配的时候有先从内存池获取,内存池不够了才会加锁全局内存池申请,减少系统调用避免线程间的锁竞争
    • 内存回收没有真正释放给操作系统,而是放回预先分配的大块内存中。只有当闲置内存太多的时候才会返还一些给操作系统

    内存管理主要组件有mspan、mcache .mcentral和mheap

    • mspan是内存管理的一个基本单元
    • mcache是线程私有的,每一个goroutine都有一个mcache,一个mcache有多个- mspan。
    • mcentral是一个中心缓存,全局的,里面有不同规格的mspan,每个mspan分为空闲的和非空闲的两类。
    • mheap是一个全局的页堆,和中心缓存一样有一把琐,里面也有mspan和mcentral

    分配对象

    • 微对象(0-16K):先使用线程缓存上的微型分配器,再依次从线程缓存、中心缓存和堆分配内存
    • 小对象(16-42K):依次从线程缓存、中心缓存和堆分配内存
    • 大对象(32-∞):直接堆内存分配

    分配的流程

    • 先计算需要的大小规格
    • 使用mcache对应大小的块分配
    • 如果mcentral没有足够大的,找mheap申请一个合适的mspan
    • 如果mspan太大了,会按照要求切分,返回所需的内存,剩余的页重新构造一个mspan放回mheap
    • 如果没有合适的mspan,mheap会找操作系统申请新页,最小1MB
    内存逃逸

    每个函数都拥有自己的内存空间存放入参、局部变量和返回地址等,会由编译器在栈上分配,每个函数都有一个栈帧,函数运行结束后销毁。但是有时候需要在函数结束后仍然使用这个栈,就需要将其在堆上分配,从栈上逃逸到堆就叫内存逃逸。
    在栈分配的内存由系统申请和释放,不会带来额外的开销。堆上分配到内存则需要进行GC,会带来一定的性能开销。
    编译器会自行决定变量是否被外部引用从而是否逃逸
    如果外部函数没有引用,就优先在栈里;
    如果外部函数引用了,必定在堆上分配
    如果栈放不下,就在堆里。

    GC

    编译器可以自动回收释放栈分配的内存,堆是程序共享的内存,需要使用GC进行回收。
    分为两个半独立的组件:赋值器(就是用户态代码)和回收器(负责垃圾回收的代码)
    三色标记算法
    所有对象初始化都是白色对象
    从根对象开始扫描所有根对象,标记为灰色。(根对象是当前所有全局变量和goroutine中的栈中对象)
    从灰色对象中取出一个对象,看他有没有引用其他对象,没有的话标记为黑色,放入黑色队列。如果引用了,被引用的对象也被标记为灰色,同时该引用对象标记为黑色,放入黑色对象。也就是所有灰色都会被放在黑色里面。
    如果灰色队列不空的话,继续上面遍历扫描,最终内存中只有黑色和白色对象,将所有的白色对象清理了。
    Go
    Go的GC主要分为几个步骤

    • Mark:标记对象,分为两个阶段Mark Prepare和GC Drains,减少STW时间。
      • Mark Prepare:初始化GC任务,开启写屏障和辅助GC,统计跟对象个数,需要STW
      • GC Drains:扫描所有的root对象,扫描的时候相应goroutine需要暂停运行,将其加入到灰色队列,然后需要循环扫描灰色队列的对象,直到其为空
    • Mark Termination:完成标记工作以后,重新扫描全局指针和栈,因为在刚才标记的过程中可能会有新的对象分配和内存赋值。通过写屏障记录下来,使用re-scan检查,也会STW
    • Sweep:按照标记结果回收白色对象,这一步是后台运行的。

    反射

    # Go的反射
    反射指的是程序运行期间对程序本身进行访问和修改的能力,能够在运行时动态获取变量各种信息。
    go反射通过接口的类型信息实现:当向接口变量赋予一个实体类型的时候,接口会储存实体的类型信息和值信息,在运行时可以利用反射:

    • 获取变量的类型信息,如Type,如果是结构体还能获取到具体字段信息
    • 获取变量的值信息或者实例信息value
    • 还可以修改变量的值,调用关联方法

    Type是reflect的一个interface、Value是reflect的一个struct,Value实现了Type接口
    提供有两个主要方法

    • ValueOf:获取变量值信息,Value
    • TypeOf:获取变量类型信息,Type

    通过反射获取接口变量的类型信息

    如果是结构体只能通过获取结构体类型变量的基础类型Type获取其字段信息,如果不是指针,直接reflect.TypeOf()获取concreate type;如果是指针,需要Type.Elem()获取基础类型
    这里有两种分类

    • Type原生数据类型:int、string、bool、float32、以及type定义的类型,reflect.Type.Name()
    • Kind对象归属的品种:Int、Bool、Float32、Chan、String、Struct、Ptr、Map、Inteface、Slice、Array、、Unsafe Pointer等

    例如type Person struct,Person就是Type,struct是kind
    通过反射获取接口变量的值信息Value

    注意点

    就在写代码的时候使用println和fmt.Println,然后发现输出的顺序不一致,我还以为是没有输出,然后才发现好像是函数的问题

    这两个函数的本质其实都不一样,前者会重定向输出到stderr标准错误,字体颜色都不一样,vscode中是红色。这个一般用于程序启动和调试,Go内部使用。不接受数组和结构体参数,对于不同类型的组合参数比如用逗号连接好多变量,输出的是地址

    后者会重定向输出到stdout标准输出,是正常颜色字体,vscode中是蓝色。是有返回值的,返回写入的字节数和遇到的任何写入错误

    如果一个实参由String() string和Error() string方法,fmt和log库打印的时候会调用这两个方法,内置的print和println会忽略这些参数。

    面经

    www.topgoer.com

    new和make区别

    new是初始化一个指向类型的指针,new是内建函数,参数是一个类型不是一个值,返回值是指向这个类型0值的新分配的指针

    make作用是初始化切片slice、集合map或者通道chan并返回其引用,也是内建函数。第一个参数是类型,第二个是长度,返回值是一个类型。

    make只用来创建slice、map和chan并返回类型是T的初始化实例。

    Printf()、Sprintf()和Fprintf()
    都是格式化输出字符串,比如使用占位符%d之类的。

    • Printf():是把格式化字符串输出到标准输出,一般是屏幕,可以重定向
    • Sprintf():把格式化字符串输出到指定字符串中,比上面多一个char,是目标字符串的地址
    • Fprintf():格式化输出到一个stream流,一般输出到文件中

    数组和切片的区别
    数组是具有固定长度且有零个或多个相同类型元素的序列,数组长度是数组类型的一部分,[3]int和[4]int是两个类型。
    数组需要指定大小,否则会根据初始化自动确定,不可变,是值传递,作为参数直接传入的时候,是拷贝一份作为形参,而不是直接引用地址。
    切片是储存相同类型元素的一个可变长度的序列,是一个轻量级数据结构,包含有指针、长度和容量。切片可以使用数组或者make初始化,可以不指定长度

    go终端常用命令

    • go env:查看go的环境变量,比如如果需要更换代理就可以在这里查看
    • go run:编译并运行go源码文件
    • go build:编译源码文件
    • go get:动态获取远程代码包
    • go install:编译go文件并安装到bin、pkg目录下
    • go clean:清理工作目录,删除遗留的目标文件
    • go version:查看版本

    go的协程
    协程和线程都可以实现程序的并发。
    go中可以直接使用go关键词创建一个协程go runtine,在协程之间可以使用channel进行通信。
    go的引用类型包含有哪些
    数组切片、字典、通道和接口interface{}
    go的指针运算*
    和c语言差不多,&和*,前者对变量取地址,后者是取得指针所指向地址的数据。
    go语言的main函数
    不能有参数
    没有返回值
    main函数所在的包必须是main包
    main函数可以使用flag包获取命令行传入的参数
    go语言同步锁
    一个goroutine获得Mutex之后,其他的goroutine只能等待,除非释放锁
    RWMutex在读锁占用的时候,会禁止写操作,可以读
    RWMutex在写锁占用的时候,会禁止任何goroutine读或者写操作,相当于全部独占

    go里面channel的特性

    • 无缓冲的channel是同步阻塞的,有缓冲区的channel是异步非阻塞的
    • 给关闭的channel发送消息,会引起panic
    • 从关闭的channel接收消息,如果缓冲区没有消息,返回0

    触发异常的场景

    • 空指针解析
    • 下标越界
    • 除数为0
    • 调用panic函数

    go语言的select机制
    用来处理异步io的,最大的一条限制就是每个case里面必须是一个io操作,在语言级别支持select关键字
    进程线程和协程的区别
    进程是资源的分配和调度的一个独立单元,线程是CPU调度的基本单元。
    一个进程里面可以有多个线程,进程结束后所有线程都会结束,线程的结束不会影响进程以及其他线程。
    线程共享进程的资源,包括寄存器、堆栈和上下文,一个进程至少有一个线程。
    进程的切换资源消耗很大,效率低,县城切线的资源消耗一般效率一般,协程是小号最小的效率最高。协程的本质是当前进程在不同函数代码中切换执行,是一个用户层面的,可以是单线程多协程也可以是多线程多协程。
    map实现有序排序
    map本身是无序的,想有序的话只能通过对key处理。比如使用slice对key进行有序存放,然后通过slice的索引在map里面取值。
    channel应用场景和原理
    # Go channel的使用场景,用法总结
    应用场景:

    • 消息传递
    • 信号广播
    • 事件订阅 和广播
    • 请求、相应转发
    • 任务分发、结果汇总
    • 并发、同步和异步

    分为三种状态:nil、active和closed
    其底层原理就是在内存中实例化了一个hchan结构体,返回一个chan指针。
    通道使用互斥锁mutex,让goroutine以FIFO的方式进入到结构体中,需要收发消息的时候,锁住结构体,缓存中的数据按照链表顺序存放,按照链表顺序读取
    如果通道满了,继续发送消息会阻塞goroutine,原理是通过Go运行时的scheduler完成调度。
    go如何实现面向对象
    面向对象的三个特点:封装、继承和多态

    • 封装:将需要抽象的字段放在结构体中,之后给结构体绑定相应的抽象方法,即可实现封装
    • 继承:没有显式继承,通过结构体中嵌套一个结构体实现组合继承
    • 多态:给多个不同类型结构体绑定相同的方法,实现多态
    继承
    type Person struct{
        name string
    }
    func (p *Person) setName(name string){
        p.name=name
    }
    func (p *Person) getName(){
        fmt.Println(p.name)
    }
    
    func main(){
        p:=Person("name")
        p.setName("ddd")
        p.getName()
    }
    //封装
    
    //多态
    type Dove struct{}
    type Eagle struct{}
    func (d *Dove) fly(){}
    func (e *Eagle) fly(){}
    
    func main(){
        var b Birds//抽象接口
        b=&Dove
        b.fly()
        b=&Eagle
        b.fly()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • 相关阅读:
    前端热门面试题二
    Java中阻塞队列原理、特点、适用场景
    每次审查 OKR时,团队要讨论的12个启发性问题
    Real-Time Rendering——9.8.2 Multiple-Bounce Surface Reflection多次反射表面反射
    禅道的原理及应用详解(一)
    实现寄生组合继承
    leetcode100相同的树,C语言时间击败百分之百
    秋招上岸“我”都做对了哪些事?
    2023年全国研究生数学建模竞赛华为杯C题大规模创新类竞赛评审方案研究
    C++双整数转双字节16进制
  • 原文地址:https://blog.csdn.net/qq_41037945/article/details/126416504