GoLang

Go基础

分享一些我的Golang笔记,已助我拿到多个18k+的Offer,在面向Golang岗位时,Golang考察占比40%,其中问内置的数据结构如Map,channel,slice占比又是最大的。

当然语言不是根本,还要配合其他计算机八股文和一些基础的算法知识。

与java的区别

  • go不允许重载
  • go速度>java
  • go没有多态
  • go通过匿名组合实现继承,java使用extends关键字,且go可以多继承,java不行
  • go有协程
  • GC使用三色标记法

数据类型与关键字

print格式化输出函数

Printf(),Sprintf(),FprintF() 虽然这三个函数,都是格式化输出,但是输出的目标不一样
Printf 是标准输出,一般是屏幕,也可以重定向。
Sprintf()是把格式化字符串输出到指定的字符串中。
Fprintf()是把格式化字符串输出到文件中。

defer

作用:

  • defer延迟函数,
  • 释放资源,收尾工作(如释放锁,关闭文件,关闭链接)
  • 捕获panic;

多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中

defer,return,return value(函数返回值) 执行顺序

  1. 首先return,
  2. 其次return value,
  3. 最后defer,且可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针

type

type可以定义结构体接口新类型定义高阶函数类型

1
2
type myint int
type mystr string
1
2
3
4
5
6
7
8
9
10
// 函数式编程
type my_fun func (int,int)(string)
//fun1()函数的返回值是my_func类型
func fun1 () my_fun{
fun := func(a,b int) string {
s := strconv.Itoa(a) + strconv.Itoa(b)
return s
}
return fun
}

引用类型与值类型

引用类型 变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过 GC 回收。包括 指针slice 切片管道 channel接口 interfacemap函数等。引用类型的变量,我们不光要声明它,还要为它分配内容空间

值类型是 基本数据类型,int,float,bool,string, 以及数组和 struct 特点:变量直接存储值,内存通常在栈中分配,栈在函数调用后会被释放,值类型的则不需要显示分配内存空间,是因为go会默认帮我们分配好

make & new

变量初始化,一般包括2步:

  1. 声明,var
  2. 内存分配,new或者make

var声明值类型的变量时,系统会默认为他分配内存空间,并赋该类型的零值比如布尔、数字、字符串、结构体

如果指针类型或者引用类型的变量,系统不会为它分配内存,默认就是**nil**。此时如果你想直接使用,那么系统会抛异常,必须进行内存分配后,才能使用。


关于make,第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间

1
2
3
4
list := make([]string, 10, 10)
list = append(list, "a")
fmt.Println(list, len(list))
// 输出[ a], 11

new 和 make 两个内置函数,主要有以下2点区别

  1. 使用场景区别:

    make 只能用来分配及初始化类型为slice、map、chan 的数据。

    new 可以分配任意类型的数据,并且置零。

  2. 返回值区别:

    make返回的是slice、map、chan类型本身,这3种类型是引用类型,就没有必要返回他们的指针

    new返回一个指向该类型内存地址的指针

rune 类型

相当int32

golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8

byte 等同于int8,常用来处理ascii字符

rune 等同于int32,常用来处理unicode或utf-8字符

单引号,双引号,反引号的区别

单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。

双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。

反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。

select

golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。

  • select 机制用来处理异步 IO 问题
  • select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操作
  • golang 在语言级别支持 select 关键字

Slice

Slice与Array的区别

array是固定长度的数组,是值类型的,如果进行赋值或者作为函数参数,实际上整个数据都会被重新拷贝一份,使用前必须声明长度。

slice切片是基于数组实现的,可以理解是为对底层数组的抽象,属于引用类型,作为函数参数时,slice传递的是指针。复制使用copy进行深拷贝,:= 是浅拷贝,slice不是线程安全的。

slice的数据结构定义如下:

1
2
3
4
5
**type slice struct {
array unsafe.Pointer //指向底层数组的指针
len int //切片长度
cap int //切片容量
}**

扩容

Go 中切片扩容的策略:

  • 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
  • 否则判断,如果旧切片的长度<1024,则最终容量就是旧容量的两倍
  • 否则判断,如果旧切片长度≥1024,则最终容量从旧容量开始循环 增加原来的 1/4, 直到最终容量大于等于新申请的容量
  • 如果最终容量计算值溢出,则最终容量就是新申请容量

注意:如果 slice 在 append() 过程中没有发生扩容,那么修改就在原来的内存中,如果发生了扩容,就修改在新的内存中。

删除元素

指定index,调用append链接前后切片append(str[:index], str[index + 1]…)

内存分配

有可能分配到栈上,也有可能分配到栈上。当开辟切片空间较大时,会逃逸到堆上。

通过命令**go build -gcflags "-m -l" xxx.go**观察golang是如何进行逃逸分析的

1
2
3
4
5
6
7
8
9
10
package main

func main() {
_ = make([]string, 200) //1
//_ = make([]string, 20000) //2
}

//output
//1. make([]string, 200) does not escape
//2. make([]string, 20000) escapes to heap

Map

map默认是并发不安全的,同时对map进行并发读写时,程序会panic,因为Go 官方认为 Go map 更应**适配典型使用场景,**而不是为了小部分情况(并发访问),导致性能损失。

map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变,所以官方避免大家依赖顺序,直接打乱处理,都会从一个随机值序号的bucket,再从其中随机的cell开始遍历,所以是无序的。

可以作为Map Key的只能是可比较的,不能作为map key 的类型包括

  • slices
  • maps
  • functions

Map的实现

Map 底层是由hmapbmap两个结构体实现的。

1
2
3
4
5
6
7
8
9
10
11
12
type hmap struct {
count int //元素个数,调用len(map)时直接返回
flags uint8 //标志map当前状态,正在删除元素、添加元素.....
B uint8 //单元(buckets)的对数 B=5表示能容纳32个元素
noverflow uint16 //单元(buckets)溢出数量,如果一个单元能存8个key,此时存储了9个,溢出了,就需要再增加一个单元
hash0 uint32 //哈希种子
buckets unsafe.Pointer //指向单元(buckets)数组,大小为2^B,可以为nil
oldbuckets unsafe.Pointer //扩容的时候,buckets长度会是oldbuckets的两倍
nevacute uintptr //指示扩容进度,buckets编号
extra *mapextra //与gc相关 可选字段
}

bmap 就是我们常说的“桶”,一个桶里面会最多装 8 个 key,低B位是相同的会落入同一个桶,高 8 位来决定 key 到底落入桶内的哪个位置

bmap 中还存储一些状态值,且都是小于minTopHash的

为了避免tophash和这些状态值相等,产生混淆,所以若tophash<minTopHash时候,自动将其值加上minTopHash作为该key的tophash

1
2
3
4
5
6
7
8
9
10
11
type bmap struct {
tophash [bucketCnt]uint8
}
//实际上编译期间会生成一个新的数据结构
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}

读取

Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。

1
2
3
4
// 不带 comma 用法
value := m["name"]
// 带 comma 用法
value, ok := m["name"]

根据 key 的不同类型/返回参数,编译器会将查找函数用更具体的函数替换

  1. 写保护监测

    map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic,这也说明了 map 不是线程安全的

  2. 计算hash

    看高8位低B位 10010111 | ... │ 01010

  3. 找到hash对应的bucket

    哈希值的低B个bit 位,用来定位key所存放的bucket,如果当前正在扩容中,并且定位到的旧bucket数据还未完成迁移,则使用旧的bucket(扩容前的bucket)

  4. 遍历bucket查找

    用步骤2中的hash值,得到高8个bit位,用来快速判断key是否已在当前bucket中,也就是10010111,转化为十进制,也就是151。在 bucket 及bucket的overflow中寻找tophash 值(HOB hash)为 151* 的 槽位,即为key所在位置

5. 返回key对应的指针

流程如下图:

Map遍历

通过对 **mapiterinit**方法阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据。

更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代

哈希冲突

比较常用的Hash冲突解决方案有链地址法和开放寻址法:

  • 链地址法
    当哈希冲突发生时,创建新单元,并将新单元添加到冲突单元所在链表的尾部

  • 开放寻址法
    当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。

    开放寻址法有多种方式:线性探测法(冲突则尝试加1)平方探测法双重哈希法。

扩容

负载因子(load factor),用于衡量当前哈希表中空间占用率的核心指标

负载因子 = 哈希表存储的元素个数/桶个数 (默认6.5)

Map的扩容条件:

  1. map元素个数 > 6.5 * 桶个数,双倍扩容
    双倍扩容:新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets
  2. 当桶总数 <2^15时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。
    当桶总数 ≥2^15时,且溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。
    此时是等量扩容
    等量扩容:并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。

对于条件2,其实算是对条件1的补充。因为在负载因子比较小的情况下,有可能 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况,如overflow的bucket数量较多,因此有了第 2 扩容条件。

搬迁 buckets 的动作在 growWork()函数中,而调用 growWork()函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作每次最多移动2个桶,每次调用都会查看是否迁移完毕,如果没有迁移完毕就尝试继续搬迁。

线程安全的Map

三种方式实现:

  • 加读写锁

    加锁方式简单

  • sync.Map

  • 分片加锁

    性能更好,可以降低锁的粒度,提高map对象的吞吐

sync.Map 采用读写分离和用空间换时间的策略保证 Map 的读写安全,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

  1. 无需初始化,直接声明即可使用
  2. 读写需要使用sync.Map提供的方法,Store(key, value)用于存储,Load(key)用于取值,Delete(key)表示删除。

优点:

适合读多写少的场景

缺点:

但是大量并发读写的情况下,锁的竞争会很激烈,导致性能降低。

如何解决这个问题:尽量减少锁的粒度和锁的持有时间,减少锁的粒度,常用方法就是分片 Shard,将一把锁分成几把锁,每个锁控制一个分片。

内存管理

GC算法

Go 的 GC 回收有三次演进过程:

  1. Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低
  2. GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。
  3. GoV1.8 三色标记法,混合写屏障机制,栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。

三色标记法

  1. 创建:白、灰、黑 三个集合
  2. 将所有对象放入白色集合中
  3. 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合 (这里放入灰色集合的都是根节点的对象)
  4. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色
  5. 重复步骤4,直到灰色中无任何对象,其中用到2个机制:
  6. 收集所有白色对象(垃圾)

混合写屏障

当gc进行中时,新创建一个对象. 按照三色标记法的步骤,对象会被标记为白色,这样新生成的对象最后会被清除掉,这样会影响程序逻辑。golang引入写屏障机制.可以监控对象的内存修改,并对对象进行重新标记。gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色

  • GC开始将栈上的对象全部扫描并标记为黑色。
  • GC期间,任何在栈上创建的新对象,均为黑色。
  • 被删除的对象标记为灰色。
  • 被添加的对象标记为灰色。

GC流程

次完整的垃圾回收会分为四个阶段,分别是标记准备、标记开始、标记终止、清理:

  1. 标记准备(Mark Setup):打开写屏障(Write Barrier),需 STW(stop the world)
  2. 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
  3. 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
  4. 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行

其他语言GC算法

  • 引用计数:每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象
    优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
    缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
    代表语言:Python、PHP
  • 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
    优点:解决了引用计数的缺点。
    缺点:需要STW,即要暂时停掉程序运行。
    代表语言:Golang
  • 分代收集:按照对象生命周期长短划分不同的代空间生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
    优点:回收性能好
    缺点:算法复杂
    代表语言: JAVA

GC什么时候触发

  1. 主动触发,手动调用 runtime.GC来触发GC,此调用阻塞式地等待当前GC运行完毕。
  2. 被动触发,分为两种方式:
    1. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC,默认100%),即当内存扩大一倍时启用GC。
    2. gcTriggerTime:当超过两分钟没有产生任何GC时,强制触发 GC。

内存逃逸

逃逸:本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。

栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。
变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销。编程语言不断优化 gc 算法,主要目的都是为了减少 gc 带来的额外性能开销,变量一旦逃逸会导致性能开销变大。逃逸分析在编译阶段完成的

编译器会根据变量是否被外部引用来决定是否逃逸

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;
  3. 如果栈上放不下,则必定放到堆上;

通过编译参数**-gcflag=-m**可以查看编译过程中的逃逸分析

内存逃逸的情况如下:

  1. 指针逃逸:方法内返回局部变量指针。
  2. 变量大小不确定:如编译期间无法确定slice的长度
  3. 栈空间不足:切片(扩容后)长度太大。
  4. 动态类型:如果函数参数为 interface{},也会发生逃逸
  5. 在 slice 或 map 中存储指针
  6. 向 channel 发送指针数据。
  7. 在闭包中引用包外的值。

调度模型

GMP模型

  • G(Goroutine):其中保存着栈,寄存器,以及指令等信息。

  • M(Machine):代表一个操作系统的主线程,是对内核级线程的封装,数量对应真实的 CPU 数,用于执行 G。M 会优先从关联的 P 的本地队列中直接获取待执行的 G。M 保存M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息

  • P(Processor):可以看作为一个局部调度器,当 P 有任务时,就需要创建或者唤醒一个系统线程来执行它队列里的任务,所以 P 和 M 是相互绑定的。

    P的数量默认是CPU核数,也可以通过GOMAXPROCS来指定数量。

    每个P都会维护一个本地队列,用于保存G,P会从队列头获取G交给M执行,执行完后放入到队列尾(如果需要继续执行)

调度流程

  1. 每个 P 有个局部队列,局部队列保存待执行的 goroutine,局部队列已经满了之后放到全局队列
  2. 每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3),M 从绑定的 P 中的局部队列获取 G 来执行
  3. 当局部队列为空时,M会从全局队列获取到本地队列来执行 G,当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P的局部队列中偷取 G 来执行,也就是称为work stealing
  4. 当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找空闲的M
  5. 当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执行

GMP 调度过程可能存在的阻塞

  1. I/O,select
  2. block on syscall
  3. channel
  4. 等待锁
  5. runtime.Gosched()

并发编程

1.互斥锁;2.读写锁;3.原子操作;4.sync.once;5. sync.atomic;6.channel

channel

channel是Go语言中的一个数据类型,可以把它看成管道,用来解决数据通信的问题。

默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。

根据通道的容量,可以将通道分为无缓冲通道缓冲通道

根据通道传输方向,还可以通道分为双向通道只读通道只写通道

常见问题

channel数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type hchan struct {
qcount uint // channel中的元素个数
dataqsiz uint // channel中循环队列的长度
buf unsafe.Pointer // channel缓冲区数据指针
elemsize uint16 // buffer中每个元素的大小
closed uint32 // channel是否已经关闭,0未关闭
elemtype *_type // channel中的元素的类型
sendx uint // channel发送操作处理到的位置
recvx uint // channel接收操作处理到的位置
recvq waitq // 等待接收的sudog(sudog为封装了goroutine和数据的结构)队列由于缓冲区空间不足而阻塞的Goroutine列表
sendq waitq // 等待发送的sudogo队列,由于缓冲区空间不足而阻塞的Goroutine列表

lock mutex // 一个轻量级锁
}

channel内部数据结构主要包含:

  • buf指向的一个底层的循环数组,只有设置为有缓存的channel才会有buf
  • sendx和recvx分别指向底层循环数组的发送和接收元素位置的索引
  • sendq和recvq分别表示发送数据的被阻塞的goroutine和读取数据的goroutine,这两个都是一个双向链表结构

hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据。

![](https://chrisyy-images.oss-cn-chengdu.aliyuncs.com/img/Untitled (6).png)

channel应用场景

channel适用于数据在多个协程中流动的场景,有很多实际应用:

  1. 任务定时
  2. 解耦生产者和消费者
  3. 控制并发数
  4. 协程间数据传递

同一个协程里,不能对无缓冲channel同时发送和接收数据,如果这么做会直接报错死锁。

对于一个无缓冲的channel而言,只有不同的协程之间一方发送数据一方接受数据才不会阻塞。channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。

悲观锁

悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制

乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量

同步原语

  1. sync.Mutex(互斥锁) 可以限制对临界资源的访问,保证只有一个 goroutine 访问共享资源
  2. sync.RWMutex (读写锁) 可以限制对临界资源的访问,保证只有一个 goroutine 写共享资源,可以有多个goroutine 读共享资源。
    使用场景:大量并发读,少量并发写,有强烈的性能要求
  3. sync.WaitGroup 可以等待一组 Goroutine 的返回。
    使用场景:并发等待,任务编排,一个比较常见的使用场景是批量发出 RPC 或者 HTTP 请求
  4. sync.Cond 可以让一组的 Goroutine 都在满足特定条件时被唤醒
    使用场景:利用等待 / 通知机制实现阻塞或者唤醒
  5. sync.Once 可以保证在 Go 程序运行期间的某段代码只会执行一次
    使用场景:常常用于单例对象的初始化场景
  6. sync.Pool可以将暂时将不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能(频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺)
    使用场景:对象池化, TCP连接池、数据库连接池、Worker Pool
  7. sync.Context
    可以进行上下文信息传递、提供超时和取消机制、控制子 goroutine 的执行

并发控制

golang控制并发有三种经典的方式:

  1. channel

    使用无缓冲的通道作为同步通道

  2. WaitGroup

    通过sync包中的WaitGroup实现并发控制,在主 goroutine 中 Add(delta int) 索要等待goroutine 的**数量,**在每一个 goroutine 中调用Done()通知完成

    1
    2
    3
    4
    5
    wg.Add(1)
    go func(url string) {
    defer wg.Done()
    http.Get(url)
    }(url)
  3. Context

    Context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理

    例如提供了超时 Timeout 和 Cancel 机制。总的来说,在下面这些场景中,可以考虑使用 Context:

    • 上下文信息传递
    • 控制子 goroutine 的运行
    • 超时控制的方法调用
    • 可以取消的方法调用

Goroutine泄露

泄露情况分类

  • channel 导致的泄露
    发送不接收,接收不发送
  • 传统同步机制导致的泄露(主要指面向共享内存的同步机制,比如排它锁、共享锁)
    WaitGroup ,如果设置了错误的任务数,也可能会导致阻塞,导致泄露发生。

channel和锁的对比

  1. 并发问题可以用channel解决也可以用Mutex解决,但是它们的擅长解决的问题有一些不同。
    channel关注的是并发问题的数据流动适用于数据在多个协程中流动的场景。
    而mutex关注的是是数据不动,某段时间只给一个协程访问数据的权限适用于数据位置固定的场景。
  2. channel的性能比锁代价要大很多

channel和共享内存有什么优劣势

Go的设计思想就是, 不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。

共享内存是在操作内存的同时,通过互斥锁、CAS等保证并发安全,
而channel虽然底层维护了一个互斥锁,来保证线程安全,但其可以理解为先进先出的队列,通过管道进行通信。

共享内存优势是资源利用率高、系统吞吐量大,劣势是平均周转时间长、无交互能力。
channel优势是降低了并发中的耦合,劣势是会出现死锁。

面向对象

结构比较

如果struct中含有不能被比较的字段类型,就不能被比较
如果struct中所有的字段类型都支持比较,那么就可以被比较。对应字段相等则认为两个式相等的

不可被比较的类型

  1. slice,因为slice是引用类型,除非是和nil比较
  2. map,和slice同理,如果要比较两个map只能通过循环遍历实现
  3. 函数类型

其他的类型都可以比较。

还有两点值得注意:

  • 结构体之间只能比较它们是否相等,而不能比较它们的大小。
  • 只有所有属性都相等而属性顺序都一致的结构体才能进行比较。

方法与函数

函数是指不属于任何结构体、类型的方法,也就是说函数是没有接收者的;而方法是有接收者的

  • 如果方法的接收者是指针类型,无论调用者是对象还是对象指针,修改的都是对象本身,会影响调用者;
  • 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;

接口

在Go语言中接口(interface)是一种类型,一种抽象的类型。

Go 引入了动态语言的便利,同时又会进行静态语言的类型检查,Go 采用了折中的做法:不要求类型显示地声明实现了某个接口只要实现了相关的方法即可,编译器就能检测到。

值接收者和指针接收者实现接口的区别

接口接收者是指针类型,则只能接受指针类型的赋值

接口是值类型,则都可以

参考:

https://www.liwenzhou.com/posts/Go/12_interface/#autoid-1-6-2

接口断言

接口断言分为安全断言非安全断言,使用非安全断言可能造成panic

1
2
s, ok := i2.(Student) //安全,断言失败,也不会panic,只是ok的值为false
s := i1.(Student) //不安全,如果断言失败,会直接panic

或者使用switch

1
2
3
4
5
6
7
8
switch ins:=s.(type) {
case Triangle:
fmt.Println("三角形。。。",ins.a,ins.b,ins.c)
case Circle:
fmt.Println("圆形。。。。",ins.radius)
case int:
fmt.Println("整型数据。。")
}

空接口应用

  • 空接口类型的变量可以存储任意类型的变量。
  • 使用空接口实现可以接收任意类型的函数参数。
  • 空接口作为map的值
1
2
3
4
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false

Go中的多态性是在接口的帮助下实现的

类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。

继承

Go语言的继承通过匿名组合完成:基类以Struct的方式定义,子类只需要把基类作为成员放在子类的定义中,支持多继承。

Java的继承通过extends关键字完成,不支持多继承。

反射

recflect是golang用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf()。

  • ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
  • TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

错误

panic

假如函数F中调用了panic语句,会终止其后要执行的代码,并会执行defer


几个容易出现panic的点:

  1. 函数返回值或参数为指针类型,nil, 未初始化结构体,此时调用容易出现panic,可加 != nil 进行判断
  2. 数组切片越界
  3. 如果我们关闭未初始化的通道,重复关闭通道,向已经关闭的通道中发送数据,这三种情况也会引发 panic,导致程序崩溃
  4. 如果我们直接操作未初始化的map并发读写,也会引发 panic
  5. 除数为0
  6. 调用 panic 函数

recover

在一个 defer 延迟执行的函数中调用 recover ,它便能捕捉在这之后的panic,然后正常处理。


GoLang
https://blog.chrisyy.top/golang-note.html
作者
chrisyy
发布于
2022年11月18日
许可协议