目录
3、GIN怎么做参数校验:go采用validator作参数校验
前言
1、当前go版本:1.19,目前更新到1.20(2023.2.1谷歌)
2、命令:
go env -w GOPROXY=https://goproxy.cn --下载不了依赖,设置代理
gf init demo -u -- 创建goframe框架项目,-u指定是否更新项目中使用goframe为最新版本
go get -u github.com/gogf/gf -- 创建后拉取依赖,也可跑go.mod文件
go mod tidy -- 添加需要用到但go.mod中查不到的模块
运行:cd demo && gf run main.go(也可直接运行main.go)
go调度,GMP状态流转
1、GMP
G-goroutine,M-线程(真正在CPU上跑的),P-调度器(调度器是M、G之间桥梁);
P层作用:1)每个P有自己的本地队列,减轻对全局队列的直接依赖,带来的效果就是锁竞争的减少,而 GM 模型的性能开销大头就是锁竞争。
2)每个P相对的平衡,在GMP模型中实现了Work Stealing算法,若P本地队列为空,会从全局队列或其他P的本地队列中窃取可运行的G来运行,减少空转,提高资源利用率
一个G一直占用资源:GMP模型会从正常模式转变为饥饿模式(类似于mutex),允许其它goroutine使用work stealing抢占
work stealing算法:指一个线程如果处于空闲状态,则帮其它正在忙的线程分担压力,从全局队列取一个G任务来执行,提高执行效率
2、go进行调度过程
1)某线程尝试创建一个G,那这个G会被安排到这个线程的G本地队列LRQ中,若LRQ满了,就分配到全局队列GRQ
2)新G尝试获取当前线程M,若无法获取,就从空闲M列表找一个,若空闲列表也没有,就创建一个M,然后绑定G与P运行
3)进入调度循环: 找到一个合适的G、 执行G、完成以后退出
3、GMP状态
G的状态
_Gidle:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值
_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中
_Grunning: 正在执行代码的goroutine,拥有栈的所有权_Gsyscall:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列
_Gwaiting:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列
_Gdead: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine。
_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在
_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。P的状态
_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)
_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止
_Pdead :当前处理器已经不被使用M的状态
自旋线程:处于运行状态但是没有可执行goroutine的线程,数量最多为GOMAXPROC,若是数量大于就会进入休眠
非自旋线程:处于运行状态有可执行goroutine的线程。
4、Go什么时候发生阻塞,阻塞时调度器会怎么做
1)用于原子、互斥量或通道操作导致goroutine阻塞,调度器将把当前阻塞的g从本地运行队列LRQ换出,并重新调度其它g
2)由于网络请求和IO导致的阻塞,Go提供了网络轮询器(Netpoller)来处理,后台用epoll等技术实现IO多路复用
其他回答
1)channel阻塞:当goroutine读写channel发生阻塞,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M
2)系统调用:当某个G由于系统调用陷入内核态,该P会脱离当前M,并更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G
3)系统监控:当某个G在P上运行时间超过10ms,或P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
4)主动让出:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行
5、runtime包介绍
是Go语言的运行时系统,提供了与底层系统交互和控制的功能,包含了内存管理、垃圾回收、协程调度等相关的函数和变量
1)num := runtime.NumGoroutine(),获取当前goroutine的数量
2)runtime.GOMAXPROCS(num int),设置线程数目,默认为CPU逻辑核数,设的太大会引起频繁的线程切换,降低性能, 即用它可限制运行时操作系统线程的数量
3)runtime.Gosched(),用于让出CPU时间片,即让出当前g的执行权限,调度器安排其它等待的任务运行,在下次某个时候从该位置恢复执行
4)runtime.Goexit(),会立即终止当前的g运行,而其它的goroutine并不会受此影响,在当前g终止运行前会先执行此g还未执行的defer语句。别在主函数调用runtime.Goexit,因为会引发panic
6、panic
1)用于处理异常的机制,当程序遇到无法处理的错误时,可以使用panic引发一个异常,中断程序的正常执行;
2)recover:用于捕获并处理panic引发的异常,使程序能够继续执行
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
}
}()
if b == 0 {
panic("division by zero")
}
fmt.Println("Result:", result)
// 执行结果:Error: division by zero -> Result: 0
7、defer
1)延迟调用机制,defer后面的函数只在当前函数执行完毕后才执行,通常用于释放资源
2)执行顺序:有多个defer,后调用的先执行;defer在return之后执行,在函数退出之前,defer也可以修改返回值
输出:defer2 defer1 0 【这样写没有改i的值,是创建了一个临时变量保存返回值,指定有名的变量i返回,如func test() (i int) {},才会修改】
func test() int {
i := 0
defer func() {
fmt.Println("defer1")
}()
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
goroutine
1、轻量级线程(学名:协程),goroutine的调度是由 Golang 运行时进行管理,golang开启并发的关键字,语法: go 函数名(参数列表 )
2、并发安全性:指在并发编程中,多个goroutine对共享资源的访问不会导致数据竞争和不确定的结果。
3、竞态条件/竞态竞争:当两个或以上goroutine访问相同资源的时候,对资源进行读/写。
可用go run -race xx.go来进行检测。
解决方法:对临界区资源上锁,或使用原子操作(atomics)等,原子操作的开销小于上锁
4、内存泄漏
暂时性内存泄露:获取长string或者切片中的一段内容时,新生成的对象和老的string或切片共用一个内存空间,导致老的string和切 片资源暂时得不到释放,就造成短暂的内存泄漏
永久性内存泄露:
1)goroutine永久阻塞而导致泄漏
2) time.Ticker未关闭导致泄漏--go的定时器
3)不正确使用Finalizer(析构函数)导致泄漏(使用:由runtime.SetFinalizer函数将对象与finalizer函数绑在一起,当对象不再被使用,可调用一个绑定的析构函数,即在对象被销毁的时候调用,做你想处理的逻辑,通常是用来垃圾回收关闭资源;因为Go的垃圾回收虽然足够强大,但却没有办法管理所有的内存,例如CGO的内存、关闭操作系统资源描述符等)
5、垃圾回收
有垃圾回收器(Garbage Collector),自动管理内存,会自动检测不再使用的对象,将其回收并释放占用的内存空间
for i := 0; i < 1000000; i++ {
_ = make([]byte, 1024)
}
time.Sleep(time.Second) // 等待垃圾回收器执行
var stats runtime.MemStats
runtime.ReadMemStats(&stats) // 读取内存统计信息,runtime包垃圾回收的实例
fmt.Println("Allocated memory:", stats.Alloc)go gc 历史演进
1)v1.3之前 标记-清除法(mark and sweep)
2)v1.5之后 三色标记法
3)v1.5-v1.7 三色标记法 + 开启写屏障
4)v1.8 三色标记法 + 混合写屏障法(写屏障、删除屏障 结合)
三色标记法工作原理
STW,开启混合写屏障,扫描栈对象1)初始所有对象都是白色
2)从root根出发扫描所有根对象,将被它引用的对象标记为灰色
3)分析灰色对象是否引用了其它对象,没引用将该灰色对象标记为黑色;有引用将它变黑色的同时,将它引用的对象也变为灰色
4)重复步骤3,直到灰色对象队列为空,此时白色对象即为垃圾对象,进行回收
STW,关闭混合写屏障
6、go内存管理
基本是参考tcmalloc来进行,go内存管理本质上是一个内存池,只是内部做了很多优化:自动伸缩内存池大小,合理切割内存块。
一个对象是分配在栈上还是堆上
Go和C++不同,Go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上
如何判断是否发生了逃逸:go build -gcflags '-m -m -l' xxx.go
逃逸的可能情况:变量大小、变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针new和make的区别
new只用于为类型分配内存,初始化为0,返回一个指向地址的指针make只用于slice,map,channel的初始化,返回的是引用
Go面向对象是如何实现
Go实现面向对象的两个关键是struct和interface
封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。
继承:继承是编译时特征,在struct内加入所需要继承的类即可:
多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量
7、确保并发安全性
1、使用互斥锁(Mutex):保护共享资源的访问,一次只允许一个goroutine访问共享资源,从而避免竞争条件
sync.Mutex.lock() // sync包的Mutex类 先锁住
defer sync.Mutex.Unlock() // 再释放自旋
并发编程的一种状态,指线程或进程在等待某个条件满足时,不会进入休眠或阻塞状态,而是通过不断检查条件是否满足来进行忙等待,直到条件满足或达到一定的等待时间(1ms,超过1ms锁进入饥饿模式)
1)好处:减少线程切换的开销,提高并发性能,然而也可能导致CPU资源浪费,因为线程持续占用CPU时间片。
2)Mutex允许自旋的条件:正常模式、积累的自旋次数小于最大自旋次数4、CPU 核数大于 1、有空闲的 P(调度器)Mutex有两种模式
normal和starvation,饥饿模式是 1.19 版本中引入的优化,目的是保证互斥锁的公平性,防止协程饿死,禁用自旋。
默认情况下,Mutex 的模式为正常模式。
1)正常模式:所有goroutine按照FIFO的顺序进行锁获取,被唤醒的和新请求锁的goroutine同时进行锁获取,通常新请求的更容易获取锁,因为它正在CPU上执行,不公平。被唤醒的goroutine没获取到锁不会立即转到等待队列的前面,而是判断是否满足自旋的条件,若满足则会自旋。
2)饥饿模式:所有尝试获取锁的goroutine进行等待排队,新请求锁的goroutine不会进行锁获取,而是加入队列尾部等待获取锁,公平性。
触发条件:当一个goroutine等待锁时间超过1毫秒,或者当前队列只剩下一个goroutine,Mutex切换到饥饿模式
比较:正常模式性能最好,goroutine可以连续多次获取锁,饥饿模式解决取锁公平的问题,但是性能会下降。原子操作、锁的区别
1)性能开销:原子操作对单个变量读写,锁用于对一段代码或一组操作的访问进行同步,开销更高
2)使用方式:原子操作通过硬件指令或特定的原子操作函数来实现,无需额外代码,锁要编码,用sync.Mutex
2、使用原子操作(Atomic Operations):对于简单的读写操作,可以使用原子操作来保证操作的原子性,避免竞争条件
3、使用通道(Channel):通过使用通道来进行goroutine之间的通信和同步,避免共享资源的直接访问。
1) c := make(chan int) // 无缓冲channel对象:发送和接收需要同步,发送阻塞直到数据被接收,接收阻塞直到读到数据
go sum(s[:3], c) // 跟go开启的线程一起使用2) c2 := make(chan int, 10) // 有缓冲channel:不要求发送和接收操作同步,当缓冲满时发送阻塞,当缓冲空时接收阻塞
c <- sum // 把 sum值发送到通道 c
x, y := <-c, <-c // 从通道 c 中接收值channel线程安全,底层实现如下
1)channel在src/runtime/chan.go中,内部是一个循环链表,包含buf, sendx, recvx, lock ,recvq, sendq几个部分;
2)buf:有缓冲的channel才有,用来存储缓存数据,是个循环链表;
3)sendx、recvx:记录buf这个循环链表中发送或者接收的index;
4)lock是个互斥锁;
5)recvq、sendq:分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列,是个双向链表。select
处理通道操作的一种机制,可以同时监听多个通道的读写操作,并在其中任意一个通道就绪时执行相应的操作,这在处理并发任务时非常有用如果有多个
case
同时可执行,则随机选择其中一个;若没有任何可执行的case
,则会执行default
分支(如果存在),或者阻塞等待直到至少有一个case
可执行为止func selectUse() { ch1 := make(chan int) ch2 := make(chan int) go func() { ch1 <- 10 }() go func() { ch2 <- 20 }() select { case num, ok := <-ch1: if ok { fmt.Println("Received from ch1:", num) } else { fmt.Println("通道已被关闭") } case num := <-ch2: fmt.Println("Received from ch2:", num) default: fmt.Println("没有接收到数据,走 default 分支") } }
4、使用同步机制:如等待组(WaitGroup)、条件变量(Cond)等来协调多个goroutine的执行顺序和状态。
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
res.CheckHouse = 123
}()
数据结构
1、指针:是一种变量存储另一个变量的内存地址。通过指针,可以直接访问和修改变量的值,而不是对变量进行拷贝
&:对变量取地址,如&a;*:对指针取值,如*&a,就是a变量所在地址的值,也就是a的值。所以 :*&可以抵消掉,a=*&a
x := 10
y := 20
swap(&x, &y)
fmt.Println("after swap:", x, y)
func swap(a, b *int) {
temp := *a
*a = *b
*b = temp
}
2、数组和切片
1)数组定义 加三点...会根据值自动确定长度
arr1 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
arr2 := [3]float32{1000.0, 2.0, 3.4}
2)切片:动态数组,建的时候长度空着,append追加,copy
num1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
num2 := make([]int, 0, 5) // make([]type, length, capacity),capacity可选参数
num2 = append(num2, 2)
// 拷贝 num2 的内容到 num3(但新切片得是之前切片的两倍容量,不加也行)
num3 := make([]int, len(num2), (cap(num2))*2)
copy(num3, num2)
// 移除值,只有用append连接前后两段
index := 2
numbers = append(numbers[:index], numbers[index+1:]...)slice是怎么扩容
1)Go <= 1.17:当前容量小于1024,所需容量若大于原来容量2倍,当前容量加上所需容量;否则当前容量乘2
如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。
2)Go1.18之后:由Go语言的内存管理模块返回给你需要的内存块,通常这些内存块都是预先申请好,并且被分为常用的规格,比如8,16, 32, 48, 64bytes等
5、map:无序键值对集合,也叫字典,map中键唯一,值可以重复
1)两种创建方式:
map1 := make(map[string]string)
map1["France"] = "巴黎"
map2 := map[string]string{"France": "Paris", "Italy": "Rome"}
for country := range map1 {
fmt.Println(country, "首都是", map1[country])
}
2)map无序,每次遍历map顺序可能不同。如果需要按特定顺序遍历map,将键值存到切片里,再sort排序键值切片,再遍历切片取map值
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
3)map的底层实现:go的map和C++map不一样,底层实现是哈希表,包括hmap和bucket(最重要,buckets是一个指针,最终它指向的是一个结构体)
type bmap struct {
tophash [bucketCnt]uint8
}
每个bucket固定包含8个key和value,是一个固定大小的连续内存块,分成四部分:每个条目的状态,8个key值,8个value 值,指向下个bucket的指针。
map查找:将key哈希后得到64位(64位机)用最后8个比特位计算在哪个桶,然后在 bucket 中,再去遍历 bucket 中的 key。map删除、清空:delete(m, key); a = make(map[string]int),直接重新make,长度设为0
两种清空方式比较:
1、map数量级在10w以内,make方式速度更快,但是内存消耗更多
2、map数量级大于10w,delete速度更快,且内存消耗更少
3、对于不再使用的map,直接使用make方式,长度为0清空更快
map删数据后,map的内存是否会自动释放
删除的元素是值类型,如int,float,bool,string,数组和struct,内存不会自动释放;删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用
相关语法
1、interface{}转string -》 str := fmt.Sprintf("%v", v)
int转string另一种方式:fmt.Sprintf("%d", int32(111)),%f-float转string
int64转string:strconv.FormatInt(int64(info), 10),10表示10进制,有2-36进制
int转string:strconv.Itoa(info),底层调用的就是FormatInt方法
func Itoa(i int) string {
return FormatInt(int64(i), 10)
}
float转string:strconv.FormatFloat(3.1415926, "f", 5, 64) // 3.14159
strconv.FormatFloat(浮点数,转为字符串后字符串的类型-无指数,精度,bitSize)
string转int:strconv.Atoi("1234")
string 转 int32 int64:strconv.ParseInt("123",10, 32/64),都是返回int64
strconv.ParseFloat("123.213", 64),都是返回float64,因为不会丢失精度
获取变量类型:reflect.TypeOf(str2)
var arg interface{}
// 断言方式转string
if value, ok := arg.(string); ok {
fmt.Println("类型判断成功", value)
}
// 断言interface转任何类型
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
2、5种拼接方法 a := []string{"a", "b", "c"}
1)+ :ret := a[0] + a[1] + a[2]
2)fmt.Sprintf: fmt.Sprintf("%s%s%s", a[0],a[1],a[2])
3)strings.Builder:var sb strings.Builder -》sb.WriteString(a[0]) -》sb.String()
4)bytes.Buffer:buf := new(bytes.Buffer) -》buf.Write(a[0]) -》 buf.String()
5)strings.Join:strings.Join(a,"")
性能比较:strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf
3、rune 类型:int32的别名,type rune = int32,处理中文(中文占3个字节),返回采用 UTF-8 编码的 Unicode 码点,还一个别名:type byte = uint8(uint8占一个字节,uint16以此类推)当然也可以通过 type
加等号自定义声明更多的类型别名。
作用:
1)统计字符串长度:fmt.Println(len("Go编程")) // 8,是底层占用字节长度,不是字符串长度
fmt.Println(len([]rune("Go编程"))) // 转换成 rune 数组后统计,输出:6,真正字符串长度
2)截取字符串:s := "Go编程" // fmt.Println(s[0:5]) // 输出:Go编,也行,但是要先算字节,算错了中文还会乱码;不算字节的方式:fmt.Println(string([]rune(s)[:3])) // 输出:Go编
4、利用反射:获取一个结构体的所有tag
/输出:Name json:Name
Publications json:Publication,omitempty
type Author struct {
Name int `json:Name`
Publications []string `json:Publication,omitempty`
}
func main() {
t := reflect.TypeOf(Author{})
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Name
s, _ := t.FieldByName(name)
fmt.Println(name, s.Tag)
}
}
5、用反射比较两个切片、map是否相等,非常耗性能
m1 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
m2 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
if reflect.DeepEqual(m1, m2) {
fmt.Println("相等")
}
6、结构体打印时:fmt.Printf("%#v", auther)
%v输出结构体各成员的值;
%+v输出结构体各成员的名称和值;
%#v输出结构体名称和结构体各成员的名称和值 // main.Author{Name:1, Publications:[]string{"a", "b"}}
7、空struct{} 用途:不占任何空间,减少内存分配 8、init() 函数什么时候执行
1)执行顺序:import –> const –> var –>init()–>main()
2)import导入的初始化顺序:由runtime初始化每个导入包,不是按照从上到下,而按照解析的依赖关系,没有依赖的包最先初始
3)一个文件可有多个init(),init()没有入参和返回值,不能被其他函数调用,同一个包内多个init()函数的执行顺序不作保证
7、HTTP keep-alive :也称为 HTTP 长连接,它通过重用一个 TCP 连接来发送/接收多个 HTTP请求,来减少创建/关闭多个 TCP 连接的开销,是客户端和服务端的一个约定,如果开启 keep-alive,则服务端在返回 response 后不关闭 TCP 连接;
在 HTTP/1.0 协议中,如果请求头中包含:Connection: keep-alive,则代表开启 keep-alive,默认开启 keep-alive,除非显式地关闭它:Connection: close
同样的,在接收完响应报文后,客户端也不关闭连接,发送下一个 HTTP 请求时会重用该连接
微服务,etcd,gin,gorm,gRPC等模型或框架
1、服务发现:客户端发现和服务端发现
客户端发现模式:客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求
服务端发现模式:客户端通过负载均衡器向某个服务提出请求,负载均衡器查询服务注册表,并将请求转发到可用的服务实例
微服务框架:Istio--Google、IBM和Lyft开源的微服务管理、保护和监控框架;go-zero--集成了各种工程实践的 web 和 rpc 框架
基本的负载均衡算法:随机、轮询等
2、etcd
1)一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。
2)它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障。
3)etcd 是用Go语言编写的,它具有出色的跨平台支持,小的二进制文件和强大的社区。etcd机器之间的通信通过Raft共识算法处理
3、GIN怎么做参数校验:go采用validator作参数校验
安装:
1)使用Mod依赖管理工具,在项目根目录执行:go mod init
2)然后项目根目录下会生成 go.mod文件,修改文件添加gin相关依赖require github.com/gin-gonic/gin v1.6.3
封装简单,自身的
net/http
func main() { //1.创建路由 r := gin.Default() //2.绑定路由规则,执行的函数 r.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "Hello World!") }) //3.监听端口,默认8080 r.Run(":8080") }
它具有以下独特功能:
1)使用验证tag或自定义validator进行跨字段Field和跨结构体验证。
2)允许切片、数组和哈希表,多维字段的任何或所有级别进行校验。
3)能够对哈希表key和value进行验证
4)通过在验证之前确定它的基础类型来处理类型接口。
5)别名验证标签,允许将多个验证映射到单个标签,以便更轻松地定义结构体上的验证
6)gin web 框架的默认验证器;
4、中间件
1)Middleware通常是一小段代码,它们接受一个请求,对其进行处理,每个中间件只处理一件事情,完成后将其传递给另一个中间件或最终处理程序,这样就做到了程序的解耦。
2)用它做了context拦截器、添加操作日志:group.Middleware(service.Middleware().Ctx, service.Middleware().Auth, service.Middleware().AddOperateLog)
5、项目优雅的启停
go1.8采用Http.Server内置的Shutdown方法支持优雅关机,fvbock/endless可以实现优雅重启。
6、持久化
将要保存的字符串写到硬盘等设备
1)最简单:采用ioutil的WriteFile()方法将字符串写到磁盘上,这种方法面临格式化方面的问题。
2)更好的做法:将数据按照固定协议进行组织再进行读写,比如JSON,XML,Gob,csv等。
3)如果要考虑高并发和高可用,必须把数据放入到数据库中
7、atomic底层实现
1)atomic采用CAS(CompareAndSwap 比较并交换)方式实现。CAS是使用了CPU中的原子性操作,同时完成 读取内存、比较是否相等、修改内存 这三个步骤
2)假设内存中原数据V,旧预期值A,需要修改的新值B,在操作共享变量时,CAS不对其加锁,是通过类似于乐观锁的方式进行检测,总是假设被操作的值未曾改变(即与旧值相等),确认假设真实性后立即进行值替换;本质是不断占用CPU资源来避免加锁的开销
3)cas可实现原子类、自旋锁,会产生ABA问题
1、两个线程 t1、t2,共享变量 num,其初始值为 A
2、线程 t1 使用 CAS 把值改成 B,步骤是:
1)读取 num 的值,记录到 oldNum 变量中
2)使用 CAS 判定当前 num 是否为 A,是则修改为 B但在 t1 执行这两个操作之间,线程 t2 可能进行了某种骚操作将 num 的值修改成了 B 又修改成了 A,单单对修改 num 值来说最后又改回了A,所以对t1没影响,但有可能产生bug;
取款扣钱异常情况:
小明有100块,想从 ATM 取款机上取 50,假设 ATM 创建了两个线程 t1 和 t2 并发执行 -50 的扣款过程;我们期望 t1 线程扣款成功,t2 线程扣款失败,使用 CAS 处理扣款过程
步骤:
1)t1 和 t2 都获取当前存款为100,期望更新为50,假设 t1 先执行,t2 阻塞等待
2)t1 执行,扣款成功,当前存款剩余50
3)在 t2 执行前,有人给小明转账了50,此时存款又变100
4)t2 执行,发现当前存款是100 ,与一开始的100相同,所以扣款成功,导致扣款两次,(正常情况,发现是50,是返回扣款失败)解决ABA,在原来的基础上引入一个版本号,具体操作如下:
1)CAS 读取旧值时,也需要读取一个版本号
2)修改数据时,进行版本号的判断,如果当前版本号与之前的相同,则修改,且版本号 + 1;如果当前版本号比之前的要高,则认为已经修改过,不作处理上述扣款过程,引入版本号后解决,具体如下:
1)t1 和 t2 都获取当前存款为100,期望更新为50,假设 t1 先执行,t2 阻塞等待,默认读取的版本号初始为 1
2)t1 执行,扣款成功,当前存款剩余50,并进行版本号 + 1 的操作,此时 版本号为 2
3)在 t2 执行前,有人给小明转账了50, 版本号 + 1,此时 版本号为 3
4)t2 执行,发现当前存款是100 ,与一开始的100相同,但当前版本号为 3,高于旧值,所以不进行修改操作,余额依然为 100
8、go设计模式
创建型模式:提供创建对象的机制, 增加已有代码的灵活性和可复用性。
1)工厂方法模式:即Java的类实现接口
2)抽象工厂模式:抽象工厂模式是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。
3)单例模式:一种创建型设计模式,一个类仅有一个实例,并提供访问该实例的全局节点,用sync.Once 方法
type Logger struct {
}
var logger *Logger
var once sync.Once
func getLoggerInstance() *Logger {
if logger == nil {
once.Do(
func() {
logger = &Logger{}
})
}
return logger
}
sync.Once内部原理是使用了一个互斥锁,每次检查该变量有无被分配(是否为nil),只有为nil才初始化实例。
4)生成器模式:允许你分步创建复杂的对象
5)原型模式:使你能够复制已有对象,无需使代码依赖它们所属的类。
结构型模式:介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
行为模式:负责对象间的高效沟通和职责委派。
9、go的调试/分析工具
go cover : 测试代码覆盖率;
godoc: 用于生成go文档;
pprof:用于性能调优,针对cpu,内存和并发;
race:用于竞争检测; (go run -race xx.go 应该是这样用)
10、context包
1)context用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。
2) Context其实就是一个接口,定义了四个方法
11、grpc
1)是一个现代开源的高性能远程过程调用 (RPC) 框架,可以在任何环境中运行。它可以通过对负载平衡、跟踪、健康检查和身份验证的可插拔支持有效地连接数据中心内和跨数据中心的服务。它也适用于分布式计算的最后一英里,将设备、移动应用程序和浏览器连接到后端服务。
2)和http区别:
- rpc是远程过程调用,就是本地去调用一个远程的函数,而http是通过 url和符合restful风格的数据包去发送和获取数据;
- rpc一般使用的编解码协议更加高效,比如grpc使用protobuf编解码,而http一般使用json进行编解码,数据相比rpc更加直观,但是数据包也更大,效率低下;
- rpc一般用在服务内部的相互调用,而http则用于和用户交互;
相似点:
都有类似的机制,例如grpc的metadata机制和http的头机制作用相似,而且web框架,和rpc框架中都有拦截器的概念。grpc使用的是http2.0协议。