Go 语言笔试面试题汇总
一.showmebug面试题
为什么协程比线程轻量?
- go协程调用跟切换比线程效率高
进程 系统进行资源分配的基本单位,有独立的内存空间
线程 cpu调度和分派的基本单位,线程依赖进程,共享父进程的 资源
协程 是用户态 的 轻量级线程,协程的调度完全由用户控制 。协程 间切换只需要保存上下文,没有内核的开销
(线程并发执行流程:
线程是内核对外提供的服务,应用程序可以通过系统调用让内核启动线程,由内核来负责线程调度和切换。线程在等待IO操作时线程变为unrunnable状态会触发上下文切换。现代操作系统一般都采用抢占式调度,上下文切换一般发生在 时钟中断和系统调用返回前,调度器计算当前线程的时间片,如果需要切换就从运行队列中选出一个目标线程,保存当前线程的环境,并且恢复目标线程的运行环境,最典型的就是切换ESP指向目标线程内核堆栈,将EIP指向目标线程上次被调度出时的指令地址。)
==go协程并发执行流程:不依赖操作系统和其提供的线程,golang自己实现的CSP并发模型实现:M, P, G
go协程也叫用户态线程,协程之间的切换发生在用户态。协程 间切换只需要保存上下文。 在用户态没有时钟中断,系统调用等机制,因此效率高==
-
协程非常轻量
执行go协程只需要极少的栈内存(大概是4~5KB),默认情况下,线程栈的大小为1MB。上下文切换代价小 上下文且含涉及到3个寄存器,线程切换涉及到15个寄存器
-
GPM
G保存运行堆栈,执行函数。可重用。goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行。
协程阻塞
1 原子 互斥量 和通道操作导致的 阻塞,会被切出去,然后会去执行 LRQ的其他G
2 网络请求和io导致的 阻塞。会被切到网络轮训器等待异步结果。P处理其他的G。异步结果拿到后,重新加入P。不需要 额外的M
3 系统函数操作导致的阻塞,比如文件io,P会切换到新的M去执行剩余的任务,阻塞的系统调用完成后,G重新加入P
4 后台 执行sleep导致的阻塞。golong后台监视线程sysmon,会把它标记为可以强占,LRQ优先执行。等到这个G继续执行的时候,就会被强占,同时保护现场,重新加入P的队列等待下次执行
通过GPM实现少量线程支持大量的并发,并通过网络轮训器 和sysmon来减少线程的阻塞。充分利用 有限的线程资源 ,提高go的运行效率
https://blog.csdn.net/u011957758/article/details/83018425
四、性能优化
- 性能查看工具 pprof,trace 及压测工具 wrk 或其他压测工具的使用要比较了解。
- 代码逻辑层面的走读非常重要,要尽量避免无效逻辑。
- 对于 golang 自身库存在缺陷的,可以寻找第三方库或自己改造。
- golang 版本尽量更新,这次的测试是在 golang1.12 下进行的。而 go1.13 甚至 go1.14 在很多地方进行了改进。比如 fmt.Sprintf,sync.Pool 等。替换成新版本应该能进一步提升性能。
- 本地 benchmark 结果不等于线上运行结果。尤其是在使用缓存来提高处理速度时,要考虑 GC 的影响。
- 传参数或返回值时,尽量按 golang 的设计哲学,少用指针,多用值对象,避免引起过多的变量逃逸,导致 GC 耗时暴涨。struct 的大小一般在 2K 以下的拷贝传值,比使用指针要快(可针对不同的机器压测,判断各自的阈值)。
- 值类型在满足需要的情况下,越小越好。能用 int8,就不要用 int64。
- 资源尽量复用,在 golang1.13 以上,可以考虑使用 sync.Pool 缓存会重复申请的内存或对象。或者自己使用并管理大块内存,用来存储小对象,避免 GC 影响(如本地缓存的场景)。
go
go server
go 自带的web server性能非常强悍,主要就是因为使用了协程,对于每一个web请求,服务器都会新开一个go协程去处理,一个服务器可以轻松同时开启上万个协程
4. 项目
gin框架
Gin 路由解析
路由是web框架的核心功能。通常路由实现是这样的:根据路由里的 / 把路由切分成多个字符串数组,然后按照相同的前子数组把路由构造成树的结构;寻址时,先把请求的 url 按照 / 切分,然后遍历树进行寻址。
比如:定义了两个路由 /user/get,/user/delete,则会构造出拥有三个节点的路由树,根节点是 user,两个子节点分别是 get delete。
上述是一种实现路由树的方式,且比较直观,容易理解。对 url 进行切分、比较,时间复杂度是 O(2n)。
Gin的路由实现使用了类似前缀树的数据结构,只需遍历一遍字符串即可,时间复杂度为O(n)。
通过root -> child 的形式,形成一个路由解析树。当请求进来时,如果能匹配上一条线,则进入相应的逻辑了;如果没有匹配的,直接404。
每个线程/协程占用多少内存知道吗?
goroutine 所占用的内存,均在栈中进行管理 goroutine 所占用的栈空间大小,由 runtime 按需进行分配 以
64位环境的 JVM 为例,会默认固定为每个线程分配 1MB 栈空间,如果大小分配不当,便会出现栈溢出的问题
go协程 协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。
进程、线程、协程的关系和区别:
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
如果若干线程中一个线程OOM,会发生什么?如果是Goroutine 呢? 项目 中出现过OOM吗?
达到阈值会被kill掉
goroutine 内存爆,性能分析,抓火焰图,内存分布图去看,
http://liumurong.org/2019/12/gin_pprof/
追踪bug
线上链路追踪
错误处理
error往外抛,日志记录,
try,except 中间件记录?社区不推荐
很多if err !=nil 如何优化 ?
一行代码 三行 if err !=nil
姿势一:失败的原因只有一个时,不使用error
姿势二:没有失败时,不使用error
姿势三:error应放在返回值类型列表的最后
姿势四:错误值统一定义,而不是跟着感觉走
姿势五:错误逐层传递时,层层都加日志
错误值统一定义, error信息,客户端、定位错误等
https://www.sohu.com/a/342949702_657921
https://www.cnblogs.com/zhangboyu/p/7911190.html
如果若干Goroutine有一个panic,会发生干什么?
Goroutine里面看有没有panic捕获,如果没做,整个程序都会crush掉
goroutine发生panic,只有自身能够recover,其它goroutine是抓不到的,这是常识啊。
一个defer能捕获到一个Goroutine下 子Goroutine下的panic吗?
不行,子Goroutine下做recovery捕获
gRPC gateway
gRPC gateway库,可以添加proto文件,添加描述信息,可以生成http访问方式的
proto 文件怎么管理
monorepo 全部放在一起
服务发现是怎么做的
etcd启动时 去服务地址 注册服务信息,来监听它的信息,来获取到他的每一个server地址,再用grpc load balance做负载均衡
注册中心挂了怎么办
五节点, raft算法保证一致性
gin 怎么做参数校验
类型,一些限制
package requests
import (
"net/http"
"sandan/application/utils"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9"
)
type ImMessageRequest struct {
Mode int `form:"mode" validate:"omitempty,min=0,max=10"`
}
func ImMessageVerify() gin.HandlerFunc {
return func(c *gin.Context) {
var data ImMessageRequest
//绑定数据
errA := c.ShouldBind(&data)
//校验请求数据
validate := validator.New()
//自己定义tag标签以及与之对应的处理逻辑
errB := validate.Struct(&data)
if errA != nil || errB != nil {
c.JSON(http.StatusOK, gin.H{
"errcode": utils.ERROR_VALIDATOR,
"errmsg": utils.GetMsg(utils.ERROR_VALIDATOR),
"timestamp": time.Now().UnixNano() / 1e6,
})
//终止
c.Abort()
return
} else {
//该句可以省略,写出来只是表明可以进行验证下一步中间件,不写,也是内置会继续访问下一个中间件的
c.Next()
}
}
}
绑定时 调用函数
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"gopkg.in/go-playground/validator.v9"
"regexp"
)
type DomainCheckReqest struct {
Limit int `form:"limit" binding:"required" validate:"required,min=1,max=100"`
Url string `form:"url" validate:"required,CheckUrl"`
}
//第三方网站访问记录
func DomainCheckVerify() gin.HandlerFunc {
return func(c *gin.Context) {
var data DomainCheckReqest
//绑定数据
errA := c.ShouldBind(&data)
//校验请求数据
validate := validator.New()
//自己定义tag标签以及与之对应的处理逻辑
validate.RegisterValidation("CheckUrl", CheckUrl)
errB := validate.Struct(&data)
if errA != nil || errB != nil {
c.JSON(http.StatusOK, gin.H{
"errcode": utils.ERROR_VALIDATOR,
"errmsg": utils.GetMsg(utils.ERROR_VALIDATOR),
"timestamp": time.Now().UnixNano() / 1e6,
})
//终止
c.Abort()
return
} else {
//该句可以省略,写出来只是表明可以进行验证下一步中间件,不写,也是内置会继续访问下一个中间件的
c.Next()
}
}
}
//判断url
func CheckUrl(fl validator.FieldLevel) bool {
url := fl.Field().String()
ok := govalidator.IsURL(url)
return ok
}
中间件
日志打印、限流、链路追踪开启,类似于node 剥洋葱模型
go 解析tag用的什么, 反射的原理
反射,reflect可以获取原数据, 获取里面的tag,对应的描述
原理:reflect包,通过获取的tag, value获取更详细的信息,方法等
反射:通过字符串来调用函数
golang锁
锁的几种模式,正常、饥饿模式其实是Mutex模式,并不是pmg模型,深入了解?区别?
map协程不安全,加读写锁,防止并发去修改,现在用的更多的是sync.map(协程安全)
channel有用过吗,哪些需要注意的地方✅
并发控制,select 选择,goroutine传递值等等
1.不能在close之后发送信息, 但仍然可以从channel读取数据
package main
import "fmt"
func main(){
var ch = make(chan int,10)
ch <- 1
ch <- 2
close(ch)
//上面已经将管道关闭,此处不能再像管道添加数据
//ch <- 6
//管道的关闭不影响从管道读取数据
n1 := <- ch
n2 := <- ch
fmt.Println(n1,n2)
}
官方推荐 不要用共享内存来操作变量
2.
- chan 可以声明为只读或者只写 默认是读写都可用
- 使用chan完成后记得注意关闭,不关闭阻塞会导致deadlock 如果太麻烦可以使用select
- 使用recover, 解决
协程中出现panic,导致程序崩溃
- 如果chan容量满了还在增加元素,或者chan没有元素还在获取元素,或者没关闭chan就遍历元素都是会报deadlock的.
四、数据库篇
数据库锁
mysql: 行锁、表锁:
使用的时候注意一点,不要使行锁退化成表锁
redis:
setnx key占用, 抢锁没问题,释放锁时过期了,delete别人的锁,redlock 里面有锁的vlaue值,删除时会判断是否一致,有重试次数,间隔
负载均衡算法
随机,轮训等
锁
mutex的模式
正常、饥饿
https://blog.csdn.net/csdniter/article/details/110934260
egg-tech
参数函数
f func()为指针
go 切片扩容
// src/runtime/slice.go
// go version 1.13
func growslice(et *_type, old slice, cap int) slice {
// ...省略部分
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
//原切片长度低于1024时直接翻倍
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
//原切片长度大于等于1024时,每次只增加25%,直到满足需要的容量
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ...省略部分
}
string是两部分组成的,一部分是指针+长度,另外一部分是utf-8编码的字节数组。64位下16字节
新浪面试
TCP和UDP的区别
TCP协议定义(Transimission Control Protocol)是以一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP的优点: 可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。
TCP的缺点: 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。
UDP的优点: 快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击……
UDP的缺点: 不可靠,不稳定 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。 基于上面的优缺点,
-
小结TCP与UDP的区别:
1.基于连接与无连接;
2.对系统资源的要求(TCP较多,UDP少);
3.UDP程序结构较简单;
4.流模式与数据报模式 ;
5.TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。
TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
tcp丢包问题
二、TCP协议丢包后,如何解决丢包的问题
为了满足TCP协议不丢包。TCP协议有如下规定:
-
数据分片:发送端对数据进行分片,接受端要对数据进行重组,由TCP确定分片的大小并控制分片和重组
-
到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认
-
超时重发:发送方在发送分片时设置超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片数据
-
滑动窗口:TCP连接的每一方的接受缓冲空间大小固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出
-
失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
-
重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据;
-
数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验或有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发
https://www.cnblogs.com/williamjie/p/9390164.html
四.图**视
4.1 1面技术面(问技术实现,底层偏多)
1.mysql 相关
B+树的优点在于:
1.由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。
数据存放的更加紧密,具有更好的空间局部性。
因此访问叶子节点上关联的数据也具有更好的缓存命中率。
2.B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。
而且由于数据顺序排列并且相连,所以便于区间查找和搜索。
而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于:
由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
回表
普通索引查找过程第二步
- 回表查询
先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低。
- 索引覆盖
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
例如:select id,age from user where age = 10;
如何实现覆盖索引
常见的方法是:将被查询的字段,建立到联合索引里去。
http://www.cppcns.com/shujuku/mysql/300346.html
多个单列索引和联合索引的区别
多个单列索引在多条件查询时只会生效第一个索引!所以多条件联合查询时最好建联合索引!
数据库其他知识点:
1、需要加索引的字段,要在where条件中
2、数据量少的字段不需要加索引;因为建索引有一定开销,如果数据量小则没必要建索引(速度反而慢)
3、如果where条件中是OR关系,加索引不起作用
4、联合索引比对每个列分别建索引更有优势,因为索引建立得越多就越占磁盘空间,在更新数据的时候速度会更慢。另外建立多列索引时,顺序也是需要注意的,应该将严格的索引放在前面,这样筛选的力度会更大,效率更高。
事务
悲观锁 乐观锁
乐观锁
总是假设最好的情况,每次去读数据的时候都认为别人不会修改,所以不会上锁, 但是在更新的时候会判断一下在此期间有没有其他线程更新该数据, 可以使用版本号机制和CAS算法实现。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。 在Java中java.util.concurrent.atomic包下面的原子变量类就是基于CAS实现的乐观锁。
悲观锁
总是假设最坏的情况,每次去读数据的时候都认为别人会修改,所以每次在读数据的时候都会上锁, 这样别人想读取数据就会阻塞直到它获取锁 (共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。 传统的关系型数据库里边就用到了很多悲观锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
使用场景
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
乐观锁适用于读比较少的情况下(多写场景),如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁的缺点
- 1.ABA问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题, 它可以通过控制变量值的版本来保证 CAS 的正确性。 大部分情况下 ABA 问题不会影响程序并发的正确性, 如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
- 2.自旋时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用, 第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation) 而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 3.只能保证一个共享变量的原子操作
CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。 但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性, 可以把多个变量封装成对象里来进行 CAS 操作. 所以我们可以使用锁或者利用AtomicReference类把多个共享变量封装成一个共享变量来操作。
项目为什么使用MQ(应用场景)
- 削峰填谷:
诸如秒杀、抢红包、企业开门红等大型活动时皆会带来较高的流量脉冲,或因没做相应的保护而导致系统超负荷甚至崩溃,或因限制太过导致请求大量失败而影响用户体验,消息队列 MQ 可提供削峰填谷的服务来解决该问题。 - 异步解耦:
交易系统作为淘宝/天猫主站最核心的系统,每笔交易订单数据的产生会引起几百个下游业务系统的关注,包括物流、购物车、积分、流计算分析等等,整体业务系统庞大而且复杂,消息队列 MQ 可实现异步通信和应用解耦,确保主站业务的连续性。 - 顺序收发:细数日常中需要保证顺序的应用场景非常多,比如证券交易过程时间优先原则,交易系统中的订单创建、支付、退款等流程,航班中的旅客登机消息处理等等。与先进先出(First In First Out,缩写 FIFO)原理类似,消息队列 MQ 提供的顺序消息即保证消息 FIFO。
- 分布式事务一致性:交易系统、支付红包等场景需要确保数据的最终一致性,大量引入消息队列 MQ 的分布式事务,既可以实现系统之间的解耦,又可以保证最终的数据一致性。
- 大数据分析:数据在“流动”中产生价值,传统数据分析大多是基于批量计算模型,而无法做到实时的数据分析,利用阿里云消息队列 MQ 与流式计算引擎相结合,可以很方便的实现将业务数据进行实时分析。
- 分布式缓存同步:天猫双 11 大促,各个分会场琳琅满目的商品需要实时感知价格变化,大量并发访问数据库导致会场页面响应时间长,集中式缓存因为带宽瓶颈限制商品变更的访问流量,通过消息队列 MQ 构建分布式缓存,实时通知商品数据的变化。
4.2 技术面2面(问技术实现较多)
http // https 区别
简历项目
问一些公司基本的情况
4.3 面技术面(主要对简历)
3.aes加密
PostgreSQL 与 MySQL比较:
5. blowfire
两次技术面,一次hr面
5.1 技术1面 项目负责人leader
5.2 2面 、技术负责人
负责人leader: 主要聊经历 技术实现等,es数据量大时成本问题等等
技术负责人: 主要聊简历,并出了一道算法题(消费者与生产者数据产生消费不同步 就报panic问题)
array [1,2,3,4,34,5,6,3,2,1] # 是一个环型1024大小数组# ,生产者扔数据, 消费者取数据, 消费者完成了之后可以,从0处继续消费
class():
p_index = 0
c_index = 0
p_round= 0 ## 生产者周期
c_round= 0 ## 消费者周期
def rpush()
def rpop()
比如:消费者一直消费数据,生产者跟不上 的判断条件, 在c_index走到最右边时(重新去队列中拿数据),这时已无数据可拿
(c.round > p.round) and (c_index < p_index)
消费者比不上生产者
(c.round < p.round) and (c_index < p_index)
排序算法
求最n位数组的 最大子数组的最大和
import random
def max_violent(arr):
max=-100
a=0
b=0
for i in range(len(arr)): #每次所取子数组的头
for j in range(i,len(arr)): #每次所取子数组的尾
tar=0
for k in range(i,j+1): #累加该数组元素
tar=tar+arr[k]
if tar>max:
max=tar
a=i
b=k
return (max,a,b)
arr=[]
for i in range(10):
arr.append(random.randint(-10,10))
print(arr)
print('暴力方法:',max_violent(arr))
def sub_lists(my_list):
subs = [[]]
for i in range(len(my_list)):
n = i+1
while n <= len(my_list):
sub = my_list[i:n]
subs.append(sub)
n += 1
return subs
六. flow
1.字符串转int
2. ‘’ 为byte
3. go语言关于值类型和引用类型
1、在go语言中,值类型和引用类型有以下特点:
a、值类型:基本数据类型,int,float,bool,string,以及数组和struct
特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放
b、引用类型:指针,slice,map,chan等都是引用类型
特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
4.Go语言参数传递是传值还是传引用
4.1 什么是传值(值传递, map, chan)
传值的意思是:函数传递的总是原来这个东西的一个副本,一副拷贝。比如我们传递一个int类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。
任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。
func main() {
i:=10
ip:=&i
fmt.Printf("原始指针的内存地址是:%p\n",&ip)
modify(ip)
fmt.Println("int值被修改了,新值为:",i)
}
func modify(ip *int){
fmt.Printf("函数里接收到的指针的内存地址是:%p\n",&ip)
*ip=1
}
4.2 什么是传引用(引用传递)
Go语言(Golang)是没有引用传递的,这里我不能使用Go举例子,但是可以通过说明描述。
以上面的例子为例,如果在modify函数里打印出来的内存地址是不变的,也是0xc42000c028,那么就是引用传递。
所以在这里,Go语言通过make函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。这里的map可以理解为引用类型,但是记住引用类型不是传引用。
指针类型可以修改,非指针类型不行
如struct ,传地址才能修改,make创建的map、chan,可以直接修改
4.3 特殊的slice
所以我们通过%p打印的slice变量ages的地址其实就是内部存储数组元素的地址,slice是一种结构体+元素指针的混合类型,通过元素array(Data)的指针,可以达到修改slice里存储元素的目的。
如:p:=Slice{cap:1,len:1,data:&i}
所以修改类型的内容的办法有很多种,类型本身作为指针可以,类型里有指针类型的字段也可以。
如 p:=Slice{cap:1,len:1,data:&i}
可以修改data字段
单纯的从slice这个结构体看,我们可以通过modify修改存储元素的内容,但是永远修改不了len和cap,因为他们只是一个拷贝,如果要修改,那就要传递*slice作为参数才可以。
https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html
random
waitgroup 如何控制并发
wait() 的时候 使用 CAS( Compare And Swap )源子操作
数据结构:
type WaitGroup struct {
noCopy noCopy
state1 [12]byte
sema uint32
}
实现原理
肯定是离不开阻塞唤醒机制和次数加减。而且是并发环境,那么次数加减要CAS。最后记录下阻塞的goroutine个数,因为要把挨个他们唤醒。
为什么要有CAS:因为通过锁实现原子操作时,其他线程必须等待已经获得锁的线程运行完以后才能获得资源,这样就会占用系统的大量资源
singleflight就是这样的例子。它解决了一堆人等一个人干完活的问题。就比如现在有100个线程同时请求数据库中同一行数。但只能有一个线程能读库,其他线程都阻塞等待它的结果。