Technical Documentation: talkgo/night
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
[Go 夜读](https://talkgo.org/)
Star History
加入方法
微信群
微信搜索 night_reading_go ,添加好友,备注你的姓名、公司、工作岗位和职责,
来自:GitHub,我会拉你入群。
在群内大家会就一些话题进行深入交流和探讨,说不定你还能遇到你的“导师”,我相信你会在里面受益良多,希望这个收益可以让你也能积极参与讨论,让微信群因你的加入而更精彩。
知识星球
2023 年,我的回顾与启航:从心出发。
2024 年,「保持好奇心,向 AI 进发」。
每一年,我们会为大家创建专门的微信群,在这里不仅会有知识星球内容提醒,还会有 Go 夜读各位讲师,你可以直接跟他们沟通。
在知识星球中,我们会放出「Go 夜读」每一期分享内容的资料。
我们的知识星球合伙人、嘉宾、资深工程师们,还会不定期的分享他们的精彩内容,以及他们的精彩分析。
除此之外,我们还筹备了以下专栏(相比于 2023 年,我们做了精简,以求做好):
- #连接你我 通过 1:1 深度连接你,帮你走出精彩的职场之路。
- #成长手记 讲述我在经营「Go 夜读」过程中的一些经历。
- #好书共读 从书中学,书中自有黄金屋,书中自有颜如玉。我不是一个阅读达人,但是我会通过 #21天读书打卡 方式,认真读完每一本书,也会认真写下我的读后感。
- #从听中学 对于一个每个月收听超过 50 小时的重度播客听众来说,有太多精彩的内容和洞察想要与人分享、交流。从听(播客、视频)中学习行业大咖的经验和总结。
- #以练代学 开源项目的实践,7 天系列,30 天系列,效率工具,效率提升等。
- #管理手记 在现代管理学之父德鲁克看来,我们每一个人都是管理者,也会分享一些管理洞见,希望可以相互交流。
- #私享会 #星友局 属于我们自己的聚会。
我们的使命、愿景、价值观
- 使命:让每个开发者都能成长
- 愿景:深度连接 10000 个开发者
- 价值观:真诚、互助、尊重
欢迎你直接联系我或者提 Issues。
订阅 Go 夜读微信公众号
Go 夜读微信公众号主要是 Go 语言相关的话题,例如 Go 新特性分析, Go 周边新闻,Go Proposal解读, Go 源码剖析,以及 Go 工程实践。
Go 夜读还包括读书会和算法学习板块,读书会偶尔会有读书笔记分享,也有共读共学的读书会活动。
算法学习主要是以题解讨论形式在小范围内进行,偶尔会分享一些题解以供学习交流。
YouTube, Twitter, Facebook, Telegram, Slack
我们的精神
开源!开源!开源!
重要的事,一定要说三遍。
希望有兴趣的小伙伴们一起加入,让我们一起把 『Go 夜读』建立成一个对大家都有帮助的开源社区。
我们的目标
我们希望可以推进大家深入了解 Go ,快速成长为资深的 Gopher 。
我们希望每次来了的人和没来的人都能够有收获,成长。
让每个想要学习的人都能参与进来,(包括初中高级 Go 工程师),
只有层次相当的人才有可能有思维的碰撞和交流,这样最终的产出也尽可能的高质量。
主题内容
Go 夜读将定期进行与 Go 语言相关的话题分享,例如源码阅读、工程实践等等。
- 如果你是一个 Go 新手,推荐阅读:Go 学习之路
- 如果你对 Go 语言的历史比较感兴趣,强烈推荐阅读:Go: A Documentary
我们的选题范围
我们的选题范围包括但不限于:
- 入门级
- 实操级
- 架构设计级
- 学习方法、习惯培养等
- 效率效能提升
- 论文研讨
我们的基本流程和分享方式
- 通过提交 Issue 的方式来收集大家想要研究的与 Go 相关的源码库或源码模块等话题;
- 提交的话题提案必须得到得到 SIG 小组的批准,并成功招募到分享人,该分享才会进入准备阶段。同样欢迎自荐话题并主动进行分享;
- 分享人准备分享材料,并在材料准备完毕后交付 SIG 小组审阅;
- 当 SIG 小组完成对材料的审阅后,将进行正式排期(这期间包括划定受众范围、审阅任务分工、分发排期计划等);
- 正式在线上进行分享;
- 将视频进行后期剪辑并上传至视频网站,再进行后续分发。
注:
报名讲师并完成分享后,我们将赠送「Go 夜读」知识星球赠票。
回看地址
- Go 夜读(YouTuBe)
- Go 夜读(Bilibili)
Go 夜读(腾讯视频)已不再更新
往期分享
如何发起分享提案?
你是否经常困扰于某些 Go 话题没有人分享或者很少人关注?自己很想深入研究,但是却是形单影只,经常半途而废呢?
机会来了!!!参考如何发起分享提案
如何参与贡献?
想要参与贡献?阅读 如何参与贡献 查看指南。
Stargazers over time
talklgo/night Star History and Stats
Contributors
Code Contributors
This project exists thanks to all the people who contribute.
Financial Contributors
Become a financial contributor and help us sustain our community. [Contribute]
Individuals
Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]
This project is supported by:
Contributors
我非常重视每一个对这个项目的贡献者,我会将贡献者列表更新到这里,目前只有提交 Pull Request 的小伙伴,但是贡献不仅仅如此,还可以包括提交 Issue 以及在社群中有所贡献的人。
贡献者自己可以提 PR ,方法如下:
- 安装
npm install --save-dev all-contributors-cli sh gen_contributors.sh
贡献类型有多种,比如:"code", "ideas","review","talk","tutorial",你可以在 .all-contributorsrc 中修改。
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
Overview (`examples/gin_examples/README.md`)
Gin Example
How to Start
make testmake pg-upmake runmake pg-down
Development Flow
1. Modfiy interfaces
2. Modfiy services
3. Modfiy repositories
4. Modfiy handlers
5. Modfiy routesTest Flow
1. Modfiy mocks
2. Modfiy handler_testNote
- Handlers only interact with Services
- Services interact with Repository (DB)
Overview (`practice/reading-go-43/README.md`)
、(-.-)、
reflect.Value 类型有一个 UnsafeAddr 方法。这个方法会返回一个 uintptr 类型的结果值。
这种行为是————————————>不安全<————————————————————的!!!!!!
UnsafeAddr 方法返回的指针值所指向的内存地址很可能会在之后的某个时刻被存入其他的东西,从而使得你对内存中数据的直接更改导致整个程序的崩溃。
这是一个被拒绝的proposal https://github.com/golang/go/issues/19752
Overview (`themes/hugo-material-docs/README.md`)
Material Docs
A material design theme for Hugo.
Quick start
Install with git:
git clone https://github.com/digitalcraftsman/hugo-material-docs.git themes/hugo-material-docsNext, take a look in the exampleSite folder at. This directory contains an example config file and the content for the demo. It serves as an example setup for your documentation.
Copy at least the config.toml in the root directory of your website. Overwrite the existing config file if necessary.
Hugo includes a development server, so you can view your changes as you go -
very handy. Spin it up with the following command:
hugo serverNow you can go to localhost:1313 and the Material
theme should be visible. For detailed installation instructions visit the demo.
Noteworthy changes of this theme are listed in the changelog.
Acknowledgements
A big thank you to Martin Donath. He created the original Material theme for Hugo's companion MkDocs. This port wouldn't be possible without him.
Furthermore, thanks to Steve Francia for creating Hugo and the awesome community around the project.
License
The theme is released under the MIT license. Read the license for more information.
Overview (`content/articles/goroutine_and_channel/README.md`)
goroutine和通道
goroutine和通道
CSP并发模型
用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型
Golang 就是借用CSP模型的一些概念为之实现并发进行理论支持
process是在go语言上的表现就是 goroutine 是实际并发执行的实体,每个实体之间是通过channel通讯来实现数据共享。
goroutine的调度模型
MPG模型
M: 操作系统的主线程
P: 协程执行所需要的上下文
G:协程
Go协程和主线程
1、Go主线程(线程或者叫进程):一个Go主线程,可以起多个协程,协程就是轻量级的线程
2、Go协程的特点
有独立的栈空间
共享程序堆空间
调度由用户控制
协程是轻量级的线程【编译器做优化】
问题:
什么是栈空间&堆空间?
栈空间?
编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈。堆空间?
一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收,值得注意的是他与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表
怎么理解这段话?
注意我们此处谈到的堆和栈是对操作系统中的,这个和数据结构中的堆和栈还是又一定区别的。
栈: 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。
func F() {
temp := make([]int, 0, 20)
...
}类似于上面代码里面的temp变量,只是内函数内部申请的临时变量,并不会作为返回值返回,它就是被编译器申请到栈里面。
申请到 栈内存 好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
再来看看堆得情况之一如下代码:
func F() []int{
a := make([]int, 0, 20)
return a
}而上面这段代码,申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到 堆 上面了。
申请到堆上面的内存才会引起垃圾回收,如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。
参考文献:
后面我会单独出一章介绍Golang 堆空间&栈空间理解
goroutine入门
例子:
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test () hello world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
// test()
go test() //开启一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main () hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}运行结果:
main () hello golang1
test () hello world1
test () hello world2
main () hello golang2
test () hello world3
main () hello golang3
main () hello golang4
test () hello world4
test () hello world5
main () hello golang5
main () hello golang6
test () hello world6
test () hello world7
main () hello golang7
main () hello golang8
test () hello world8
main () hello golang9
test () hello world9
main () hello golang10
test () hello world10运行结果: 说明main这个主线程和test协程同时运行
可以画个逻辑图来说明这个情况:
逻辑图讲解:
1、主线程是一个物理线程、直接作用在CPU上、是重量级的,非常耗费CPU资源
2、协程是主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小
3、Golang的协程机制是重要的特点,可以轻松开启上万个协程
其他编程语言的开发机制一般基于线程,开启过多的线程,资源耗费大
这里就凸显了golang在并发上的优势了
设置Golang运行的CPU数
注意:
1、Go1.8之前 要进行设置下 可以更高效的利用CPU
2、GO1.8之后 默认让程序运行在多个核上 可以不用设置
这里使用的是go version go1.13.1
package main
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpunum:", cpuNum)
//可以自己设置使用多个CPU
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}管道
看一个例子来解释为什么要用到管道这个技术?
package main
import (
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(time.Second * 10)
//遍历结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}运行结果:
map[76]=0
map[81]=0
map[104]=0
map[117]=0
map[118]=0
map[124]=0
map[139]=0
map[153]=0
map[162]=0
map[2]=2
map[16]=20922789888000
....发现的问题:
多个协程 同时写 会出现资源竞争
解决思路:
加锁
全局变量加锁同步
没有对全局变量加锁,会出现资源竞争问题,代码会报错: concurrent map writes
加入互斥锁
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10)
//声明全局互斥锁
//lock 是一个全局互斥锁
//sync 表示同步
//Mutex 表示互斥
lock sync.Mutex
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠几秒合适?
time.Sleep(time.Second * 10)
//遍历结果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}遍历结果也要加入锁机制, 原因:
程序从设计上可以指定10秒执行了所有协程,但是主线程并不知道,因此底层可能仍然出现资源争夺
引入管道
前面使用全局变量加锁解决 但不完美:
主要有三个地方:
1)主线程在等待所有gorouting全部完成的时间很难确定,这里设置了10秒,仅仅是估算
2)如果主线程休眠时间长了,会加长等待时间
如果等待时间短了,可能还有goroutine处于工作状态,
这时会随着主线程的退出而销毁
3)通过全局变量加锁,也并不利用协程对全局变量的读写操作(不知道在哪里加锁、释放锁)
channel介绍
1.主要有下面几个特点:
1.Channel本质就是一个数据结构 -队列
2.数据是先进先出
3.线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全
4.channel是有类型的,一个string的channel只能存放string类型数据
2.基本使用:
定义 /声明 channel
var 变量 chan 数据类型
var intChan chan int
说明:
1)channel是引用类型
2)channel必须初始化才能写入数据、即make后才能使用3.例子
package main
import (
"fmt"
)
func main() {
var intChan chan int
intChan = make(chan int, 3)
fmt.Printf("intChan的值=%v\n", intChan) //intChan的值=0xc00001a100
}4.管道写入
例子1:
package main
import (
"fmt"
)
func main() {
var intChan chan int
intChan = make(chan int, 3)
fmt.Println()
//管道写入
intChan <- 10
num := 211
intChan <- num
//管道长度和容量
fmt.Printf("channel len=%v cap=%v", len(intChan), cap(intChan))
}例子2:
package main
import (
"fmt"
)
func main() {
var intChan chan int
intChan = make(chan int, 3)
fmt.Println()
//管道写入
intChan <- 10
num := 211
intChan <- num
//当写入数据不能超过容量,超过报错
intChan <- 50
intChan <- 98
//管道长度和容量
fmt.Printf("channel len=%v cap=%v", len(intChan), cap(intChan))
}例子3:
package main
import (
"fmt"
)
func main() {
var intChan chan int
intChan = make(chan int, 3)
fmt.Println()
//管道写入
intChan <- 10
num := 211
intChan <- num
//当写入数据不能超过容量
intChan <- 50
//管道长度和容量
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//读数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//在没有使用协程的情况下,管道数据已经全部取出,再取就会报错deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan
fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}5.管道细节总结:
1.channel只能存放指定的数据类型
2.channel的数据放满后,就不能再放入了
3.如果从channel取出数据后,可以继续放入
4.在没有使用协程的情况下,如果channel数据取完了再取, 就会报deadlock
6.channel的关闭
使用内置函数close可以关闭channel,当channel关闭后 就不能再向channel写数据
但是可以从channel读取数据
package main
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan)
intChan <- 300 //panic: send on closed channel
}7.channel的遍历
支持for-range的方式来遍历:
1.在遍历时,如果channel没有关闭,则出现deadlock
2.在遍历时,如果channel已经关闭,会正常遍历数据,遍历完后会退出遍历
package main
import "fmt"
func main() {
intChan := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i * 2
}
//遍历,不能使用普通的for循环,取出来的不是值
// for i := 0; i < len(intChan); i++ {
// fmt.Println("i=", i)
// }
//使用for-range循环,取出来的是值
close(intChan)
for v := range intChan {
fmt.Println("v=", v)
}
}协程和管道
看一个例子:
package main
import (
"fmt"
)
//write data
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Printf("writeData写数据=%v\n", i)
// time.Sleep(time.Second)
}
close(intChan)
}
//read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
// time.Sleep(time.Second)
fmt.Printf("readData 读到数据=%v\n", v)
}
//任务完成
exitChan <- true
close(exitChan)
}
func main() {
//创建两个管道
intChan := make(chan int, 10)
exitChan := make(chan bool, 1)
go readData(intChan, exitChan)
go writeData(intChan)
// time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}再看一个例子:
package main
import (
"fmt"
)
func putNum(intChan chan int) {
for i := 1; i <= 80; i++ {
intChan <- i
}
//关闭intChan
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <-intChan
//intChan取不到
if !ok {
break
}
flag = true
//判断是不是素数
for i := 2; i < num; i++ {
//说明num不是素数
if num%i == 0 {
flag = false
break
}
}
if flag {
//放入primeChan
primeChan <- num
}
}
fmt.Println("有一个primeNum 协程因为取不到数据退出")
//还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000) //放入结果
exitChan := make(chan bool, 4) //退出管道
//开启一个协程,向intChan写入1-8000
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否为素数
//如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
//关闭primeChan
close(primeChan)
}()
//遍历primeChan
for {
res, ok := <-primeChan
if !ok {
break
}
//结果输出
fmt.Printf("素数=%d\n", res)
}
fmt.Println("main主线程退出")
}运行结果:
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
素数=1
素数=2
素数=3
素数=5
素数=7
素数=11
素数=13
素数=17
素数=19
素数=23
素数=29
素数=31
素数=37
素数=41
素数=43
素数=47
素数=53
素数=59
素数=61
素数=67
素数=71
素数=73
素数=79
main主线程退出这里有个问题,就是结果显示不对:
代码里面增加休眠时间
修改后:
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int) {
for i := 1; i <= 80; i++ {
intChan <- i
}
//关闭intChan
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
time.Sleep(time.Millisecond)
num, ok := <-intChan
//intChan取不到
if !ok {
break
}
flag = true
//判断是不是素数
for i := 2; i < num; i++ {
//说明num不是素数
if num%i == 0 {
flag = false
break
}
}
if flag {
//放入primeChan
primeChan <- num
}
}
fmt.Println("有一个primeNum 协程因为取不到数据退出")
//还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000) //放入结果
exitChan := make(chan bool, 4) //退出管道
//开启一个协程,向intChan写入1-8000
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否为素数
//如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
//关闭primeChan
close(primeChan)
}()
//遍历primeChan
for {
res, ok := <-primeChan
if !ok {
break
}
//结果输出
fmt.Printf("素数=%d\n", res)
}
fmt.Println("main主线程退出")
}运行结果:
素数=1
素数=2
素数=3
素数=5
素数=7
素数=11
素数=13
素数=17
素数=19
素数=23
素数=29
素数=31
素数=37
素数=41
素数=43
素数=47
素数=53
素数=59
素数=61
素数=67
素数=71
素数=73
素数=79
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
有一个primeNum 协程因为取不到数据退出
main主线程退出代码效率
1.普通方法
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now().Unix()
for num := 1; num <= 80000; num++ {
flag := true
//判断是不是素数
for i := 2; i < num; i++ {
//说明num不是素数
if num%i == 0 {
flag = false
break
}
}
if flag {
}
}
end := time.Now().Unix()
fmt.Println("普通方法耗时=", end-start) //普通方法耗时= 3
}2.使用了协程+管道
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int) {
for i := 1; i <= 80000; i++ {
intChan <- i
}
//关闭intChan
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
// time.Sleep(time.Millisecond)
num, ok := <-intChan
//intChan取不到
if !ok {
break
}
flag = true
//判断是不是素数
for i := 2; i < num; i++ {
//说明num不是素数
if num%i == 0 {
flag = false
break
}
}
if flag {
//放入primeChan
primeChan <- num
}
}
fmt.Println("有一个primeNum 协程因为取不到数据退出")
//还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 20000) //放入结果
exitChan := make(chan bool, 4) //退出管道
start := time.Now().Unix()
//开启一个协程,向intChan写入1-8000
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否为素数
//如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
end := time.Now().Unix()
fmt.Println("使用协程耗时=", end-start) //使用协程耗时= 1
//关闭primeChan
close(primeChan)
}()
//遍历primeChan
for {
_, ok := <-primeChan
// res, ok := <-primeChan
if !ok {
break
}
//结果输出
// fmt.Printf("素数=%d\n", res)
}
fmt.Println("main主线程退出")
}3.优化版
在运行某个程序时,如何指定是否存在资源竞争问题?
方法很简单,在编译程序时,增加一个参数 -race
golang管道细节总结
细节1
package main
import (
"fmt"
)
func main() {
//管道可以声明只读或者只写
//1.在默认情况下,管道是双向
//var chan1 chan int //可读可写
//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2 <- 20
// num := <-chan2 //error
//3 声明为只读
var chan3 <-chan int
chan3 = make(chan int, 3)
// chan3 <- 20//error
num := <-chan3
fmt.Println("chan2=", chan2)
}细节2
package main
import (
"fmt"
)
func main() {
//使用select 可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2.定义一个管道 5个数据string
StringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
StringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统方法在遍历管道时候 如果不关闭会阻塞会导致deadlock
//问题在实际开发中可能我们不好确定什么时候关闭管道
//可以使用select 方法解决
// label:
for {
select {
//注意:这里如果intChan一直没有关闭不会一直阻塞而deadlock
//会自动到下一个case匹配
case v := <-intChan:
fmt.Printf("从intChan读取数据%d\n", v)
case v := <-StringChan:
fmt.Printf("从StringChan读取数据%s\n", v)
default:
fmt.Printf("都取不到\n")
// break label //跟label配合使用
return
}
}
}细节3
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
func test() {
//使用defer+recover
defer func() {
//捕获抛出的panic
if err := recover(); err != nil {
fmt.Println("test()发生错误", err)
}
}()
var myMap map[int]string
myMap[0] = "golang"
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
}
}Overview (`content/articles/how_to_test/README.md`)
title: "how to testing" date: 2018-12-25T00:00:00+08:00 author: [email protected]
HOW TO TESTING
原文/源码参考:
测试的作用:
- 验证代码是否符合预期
- 资源竞争检查:race detect
- 调优:profiling:memory/cpu
原始代码
代码功能:访客记次数。
package main
import (
"fmt"
"log"
"net/http"
)
var counter = map[string]int{}
func handleHello(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
counter[name]++
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
"'>Welcome!</h1> <p>Name: " + name + "</p> <p>Count: " + fmt.Sprint(counter[name]) + "</p>"))
}
func main() {
http.HandleFunc("/hello", handleHello)
log.Fatal(http.ListenAndServe(":8080", nil))
}运行:
go run main.go浏览器访问:
本地日志记录:
测试规范
运行测试:
- 测试:
go test - 压力测试:
go test -bench - 测试覆盖:
go test -cover
- 测试:
测试规范:
测试函数示例
go// go test or go test -v func TestXxx(*testing.T) // go test -bench func BenchmarkXxx(*testing.B)Xxx不能以小写字母开头。
测试文件规范:文件名以
_test.go结尾。在测试函数里面使用:Error,Fail或者相关的函数标示相关错误。
例子:
单元测试:
gofunc TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } ... }压力测试:
gofunc BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("hello") } }Examples:
gofunc ExampleHello() { fmt.Println("hello") // Output: hello } func ExampleSalutations() { fmt.Println("hello, and") fmt.Println("goodbye") // Output: // hello, and // goodbye }
测试用例
运行测试
使用go test运行测试。
➜ how_to_test git:(how_to_test) ✗ go test
? _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test [no test files]也可以使用Golang的TDD小工具:
goconvey安装:
go get github.com/smartystreets/goconvey介绍: GoConvey is awesome Go testing
运行:
goconvey效果截图:
测试用例
创建main_test.go,
touch main_test.go编写第一个测试用例:
func TestHelloHandleFunc(t *testing.T) {
rw := httptest.NewRecorder()
name := "zouying"
req := httptest.NewRequest(http.MethodPost, "/hello?name="+name, nil)
handleHello(rw, req)
if rw.Code != http.StatusOK {
t.Errorf("status code not ok, status code is %v", rw.Code)
}
if len(counter) != 1 {
t.Errorf("counter len not correct")
}
if counter[name] != 1 {
t.Errorf("counter value is error: visitor=%s count=%v", name, counter[name])
}
}运行测试:go test -v:
➜ how_to_test git:(how_to_test) ✗ go test -v
=== RUN TestHelloHandleFunc
INFO[0000] visited count=1 module=main name=zouying
--- PASS: TestHelloHandleFunc (0.00s)
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.015s
运行测试覆盖:go test -cover
➜ how_to_test git:(how_to_test) ✗ go test -cover
INFO[0000] visited count=1 module=main name=zouying
PASS
coverage: 62.5% of statements
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.021s
查看覆盖的代码:
#!/bin/bash
go test -coverprofile=coverage.out
go tool cover -html=coverage.out➜ how_to_test git:(how_to_test) ✗ go test -coverprofile=/tmp/coverage.out
INFO[0000] visited count=1 module=main name=zouying
PASS
coverage: 62.5% of statements
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.015s
➜ how_to_test git:(how_to_test) ✗ go tool cover -html=/tmp/coverage.out
效果图为:
绿色的表示测试代码覆盖住的,红色的表示没有覆盖。
第一个测试用例是直接测试http处理函数,我们使用了httptest.NewRecorder()创建ResponseRecorder对象,其中实现了 ResponseWriter interface。该对象在内存中记录了http response的状态。
还有一种测试方法是运行一个HTTP Server,使用HTTP Client请求该Server对应的接口。
httptest package中提供了NewServer方法,监听HandlerFunc处理函数,启动Server,启动Server的地址通过URL成员获得,例如:http://127.0.0.1:52412。需要注意的是,使用完毕后记得调用关闭:Close()。
代码如下,
func TestHTTPServer(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(handleHello))
defer ts.Close()
logrus.Infof("server url: %s", ts.URL)
testURL := ts.URL + "/hello?name=zouying"
resp, err := http.Get(testURL)
if err != nil {
t.Error(err)
return
}
if g, w := resp.StatusCode, http.StatusOK; g != w {
t.Errorf("status code = %q; want %q", g, w)
return
}
}运行测试,
➜ how_to_test git:(master) ✗ go test -v -run=TestHTTPServer
=== RUN TestHTTPServer
INFO[0000] server url: http://127.0.0.1:52506
INFO[0000] visited count=1 module=main name=zouying
--- PASS: TestHTTPServer (0.00s)
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.015s测试技巧:表格测试 (Table Based Tests)
代码如下,
func TestHelloHandlerMultiple(t *testing.T) {
tests := []struct {
name string
wCnt int
}{
{name: "zouying", wCnt: 1},
{name: "zouying", wCnt: 2},
{name: "user2", wCnt: 1},
{name: "user3", wCnt: 1},
}
for _, tc := range tests {
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/hello?name="+tc.name, nil)
handleHello(rw, req)
if rw.Code != http.StatusOK {
t.Errorf("status code not ok, status code is %v", rw.Code)
}
if counter[tc.name] != tc.wCnt {
t.Errorf("counter value is error: visitor=%s count=%v", tc.name, counter[tc.name])
}
}
}运行测试,
➜ how_to_test git:(how_to_test) ✗ go test -run=TestHelloHandlerMultiple
INFO[0000] visited count=1 module=main name=zouying
INFO[0000] visited count=2 module=main name=zouying
INFO[0000] visited count=1 module=main name=user2
INFO[0000] visited count=1 module=main name=user3
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.016s测试工具:testify
使用工具介绍各种 if {}判断,产生大量的冗余代码。
代码,
func TestHelloHandlerMultipleWithAssert(t *testing.T) {
tests := []struct {
name string
wCnt int
}{
{name: "zouying", wCnt: 1},
{name: "zouying", wCnt: 2},
{name: "user2", wCnt: 1},
{name: "user3", wCnt: 1},
}
for _, tc := range tests {
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/hello?name="+tc.name, nil)
handleHello(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, tc.wCnt, counter[tc.name])
}
}Sub Test
func TestHelloHandlerInSubtest(t *testing.T) {
tests := []struct {
name string
wCnt int
}{
{name: "zouying", wCnt: 1},
{name: "user2", wCnt: 1},
{name: "user3", wCnt: 1},
}
for _, tc := range tests {
t.Run("test-"+tc.name, func(t *testing.T) {
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/hello?name="+tc.name, nil)
handleHello(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, tc.wCnt, counter[tc.name])
})
}
}运行测试,
➜ how_to_test git:(how_to_test) ✗ go test -v . -run=TestHelloHandlerInSubtest
=== RUN TestHelloHandlerInSubtest
=== RUN TestHelloHandlerInSubtest/test-zouying
time="2018-12-23T23:07:19+08:00" level=info msg=visited count=1 module=main name=zouying
=== RUN TestHelloHandlerInSubtest/test-user2
time="2018-12-23T23:07:19+08:00" level=info msg=visited count=1 module=main name=user2
=== RUN TestHelloHandlerInSubtest/test-user3
time="2018-12-23T23:07:19+08:00" level=info msg=visited count=1 module=main name=user3
--- PASS: TestHelloHandlerInSubtest (0.00s)
--- PASS: TestHelloHandlerInSubtest/test-zouying (0.00s)
--- PASS: TestHelloHandlerInSubtest/test-user2 (0.00s)
--- PASS: TestHelloHandlerInSubtest/test-user3 (0.00s)
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 0.016sData Race Detect
多个goroutine同时访问共享数据时,如果数据不是线程安全的,那么有可能会产生data race。
HOW TO
go test -race测试代码
func TestHelloHandlerDetectDataRace(t *testing.T) {
tests := []struct {
name string
wCnt int
}{
{name: "zouying", wCnt: 1},
{name: "zouying", wCnt: 2},
{name: "user2", wCnt: 1},
{name: "user3", wCnt: 1},
}
for _, tc := range tests {
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/hello?name="+tc.name, nil)
handleHello(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, tc.wCnt, counter[tc.name])
}
}运行测试
➜ how_to_test git:(how_to_test) ✗ go test -race -v . -run=TestHelloHandlerDetectDataRace
=== RUN TestHelloHandlerDetectDataRace
time="2018-12-23T22:58:22+08:00" level=info msg=visited count=1 module=main name=zouying
time="2018-12-23T22:58:22+08:00" level=info msg=visited count=2 module=main name=zouying
time="2018-12-23T22:58:22+08:00" level=info msg=visited count=1 module=main name=user2
time="2018-12-23T22:58:22+08:00" level=info msg=visited count=1 module=main name=user3
--- PASS: TestHelloHandlerDetectDataRace (0.00s)
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 1.029s测试通过,是否证明了我们的代码是没有问题的呢?
其实并非如此,只是没有检测出来。为什么没有检测出来?
是因为没有多个goroutine同时运行,访问共同的数据。
修改代码
func TestHelloHandlerDetectDataRace(t *testing.T) {
tests := []struct {
name string
wCnt int
}{
{name: "zouying", wCnt: 1},
{name: "user2", wCnt: 1},
{name: "user3", wCnt: 1},
}
var wg sync.WaitGroup
wg.Add(len(tests))
for _, tc := range tests {
name := tc.name
want := tc.wCnt
go func() {
defer wg.Done()
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/hello?name="+name, nil)
handleHello(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, want, counter[name])
}()
}
wg.Wait()
}运行测试
/* Detailed source-code truncated for AI context efficiency. */分析报错
==================
WARNING: DATA RACE
Write at 0x00c0000a8f90 by goroutine 8:
runtime.mapassign_faststr()
/usr/local/go/src/runtime/map_faststr.go:190 +0x0
_/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test.handleHello()
/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test/main.go:14 +0x11c
_/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test.TestHelloHandlerDetectDataRace.func1()
/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test/main_test.go:144 +0x211
Previous read at 0x00c0000a8f90 by goroutine 7:
runtime.mapaccess1_faststr()
/usr/local/go/src/runtime/map_faststr.go:12 +0x0
_/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test.handleHello()
/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test/main.go:14 +0xbc
_/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test.TestHelloHandlerDetectDataRace.func1()
/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test/main_test.go:144 +0x211- goroutine 8, goroutine 7, ...
- DATA RACE
how_to_test/main.go:14:counter[name]++runtime/map_faststr.go:190
原因是因为在多个goroutine中,对map同时进行了++操作,而在go中,map又不是线程安全的(线程安全的map参考sync包中的map),需要进行保护。
修复race
如果咱们的代码中有data race,那么一般使用下面方式可以避免,
使用channel。
-
- go
messages := make(chan string, 2) messages <- "buffered" messages <- "channel"
使用mutex。
使用atomic。
Go by Example: Atomic Counters
goimport "sync/atomic" var ops uint64 for i := 0; i < 50; i++ { go func() { for { atomic.AddUint64(&ops, 1) } }() } opsFinal := atomic.LoadUint64(&ops)
引入mutex解决问题
- 增加
var mu sync.Mutex对counter map进行保护。 - 在对counter访问前进行
Lock操作,访问结束后,进行Unlock操作。
修改代码为,
package main
import (
"fmt"
"net/http"
"sync"
"github.com/sirupsen/logrus"
)
var counter = map[string]int{}
var mu sync.Mutex // mutex for counter
func handleHello(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
mu.Lock()
counter[name]++
cnt := counter[name]
mu.Unlock()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
"'>Welcome!</h1> <p>Name: " + name + "</p> <p>Count: " + fmt.Sprint(cnt) + "</p>"))
logrus.WithFields(logrus.Fields{
"module": "main",
"name": name,
"count": cnt,
}).Infof("visited")
}
func main() {
logrus.SetFormatter(&logrus.JSONFormatter{})
http.HandleFunc("/hello", handleHello)
logrus.Fatal(http.ListenAndServe(":8080", nil))
}测试代码,
➜ how_to_test git:(how_to_test) ✗ go test -v -race . -run=TestHelloHandlerDetectDataRace
=== RUN TestHelloHandlerDetectDataRace
time="2018-12-24T10:11:23+08:00" level=info msg=visited count=1 module=main name=user3
time="2018-12-24T10:11:23+08:00" level=info msg=visited count=1 module=main name=zouying
time="2018-12-24T10:11:23+08:00" level=info msg=visited count=1 module=main name=user2
--- PASS: TestHelloHandlerDetectDataRace (0.00s)
PASS
ok _/Users/zouying/src/Github.com/ZOUYING/learning_golang/how_to_test 1.025s具体参考:
Overview (`content/articles/sony-gobreaker/README.md`)
title: "Sony gobreaker熔断器源码分析" date: 2018-07-26T00:00:00+08:00 author: [email protected]
最近看了一下go-kit,发现这个微服务框架的熔断器,也是使用sony开源的作为基础。
sony开源在 github 的熔断器
在源代头注释中发现,原来sony实现的是微软2015时公布的CircuitBreaker标准,果然微软才开源界的大神。
1)微软定义的 Circuit breaker
微软的原文件在此:https://msdn.microsoft.com/en-us/library/dn589784.aspx
名不知道怎么正确翻译,直观翻译,可能叫:环形熔断器(或叫:循环状态自动切换中断器)。
因为它是在下面3个状态循环切换 :
Closed
/ \
Half-Open <--> Open
初始状态是:Closed,指熔断器放行所有请求。
达到一定数量的错误计数,进入Open 状态,指熔断发生,下游出现错误,不能再放行请求。
经过一段Interval时间后,自动进入Half-Open状态,然后开始尝试对成功请求计数。
进入Half-Open后,根据成功/失败计数情况,会自动进入Closed或Open。2)sony开源的go实现
// 从定义的错误来看,sony的应该增加了对连接数进行了限制 。
var (
// ErrTooManyRequests is returned when the CB state is half open and the requests count is over the cb maxRequests
ErrTooManyRequests = errors.New("too many requests")
// ErrOpenState is returned when the CB state is open
ErrOpenState = errors.New("circuit breaker is open")
)2.1) 通过Settings的实现,了解可配置功能:
type Settings struct {
Name string
MaxRequests uint32 // 半开状态期最大允许放行请求:即进入Half-Open状态时,一个时间周期内允许最大同时请求数(如果还达不到切回closed状态条件,则不能再放行请求)。
Interval time.Duration // closed状态时,重置计数的时间周期;如果配为0,切入Open后永不切回Closed--有点暴力。
Timeout time.Duration // 进入Open状态后,多长时间会自动切成 Half-open,默认60s,不能配为0。
// ReadyToTrip回调函数:进入Open状态的条件,比如默认是连接5次出错,即进入Open状态,即可对熔断条件进行配置。在fail计数发生后,回调一次。
ReadyToTrip func(counts Counts) bool
// 状态切换时的熔断器
OnStateChange func(name string, from State, to State)
}2.2)核心的*执行函数*实现
要把熔断器使用到工程中,只需要,实例化一个gobreaker,再使用这个Execute包一下原来的请求函数。
func (cb *CircuitBreaker) Execute(req func() (interface{}, error)) (interface{}, error) {
generation, err := cb.beforeRequest() //
if err != nil {
return nil, err
}
defer func() {
e := recover()
if e != nil {
cb.afterRequest(generation, false)
panic(e) // 如果代码发生了panic,继续panic给上层调用者去recover。
}
}()
result, err := req()
cb.afterRequest(generation, err == nil)
return result, err
}2.2 关键 func beforeRequest()
函数做了几件事:
- 函数的核心功能:判断是否放行请求,计数或达到切换新条件刚切换。
- 判断是否Closed,如是,放行所有请求。
- 并且判断时间是否达到Interval周期,从而清空计数,进入新周期,调用toNewGeneration()
- 如果是Open状态,返回ErrOpenState,---不放行所有请求。
- 同样判断周期时间,到达则 同样调用 toNewGeneration(){清空计数}
- 如果是half-open状态,则判断是否已放行MaxRequests个请求,如未达到刚放行;否则返回:ErrTooManyRequests。
- 此函数一旦放行请求,就会对请求计数加1(conut.onRequest()),请求后到另一个关键函数 : afterRequest()。
2.3 关键 func afterRequest()
- 函数核心内容很简单,就对成功/失败进行计数,达到条件则切换状态。
- 与beforeRequest一样,会调用公共函数 currentState(now)
- currentState(now) 先判断是否进入一个先的计数时间周期(Interval), 是则重置计数,改变熔断器状态,并返回新一代。
- 如果request耗时大于Interval, 几本每次都会进入新的计数周期,熔断器就没什么意义了。
代码的核心内容
- 使用了一个generation的概念,每一个时间周期(Interval)的计数(count)状态称为一个generation。
- 在before/after的两个函数中,实现了两个状态自动切换的机制:
- 在同一个generation(即时间)周期内,计数满足状态切换条件,即自动切换;
- 超过一个generation时间周期的也会自动切换;
- 没有使用定时器,只在请求调用时,去检测当时状态与时间间隔。
Overview (`content/articles/sync/README.md`)
title: 说明
内容均来自OSC_梦朝思夕博客,51CTO_梦朝思夕博客
Index (`content/_index.md`)
title: "Go 夜读" date: 2018-11-15T12:32:37+08:00 weight: 1
Articles Index (`content/articles/_index.md`)
title: "分享文章" date: 2018-11-15T12:32:37+08:00 weight: 5
Discuss Index (`content/discuss/_index.md`)
title: "夜读讨论" date: 2018-11-15T12:32:37+08:00 weight: 5
Harvest Index (`content/harvest/_index.md`)
title: "你的收获" date: 2019-01-10T11:00:00+08:00 weight: 8
Interview Index (`content/interview/_index.md`)
title: "面试专题" date: 2018-11-15T12:32:37+08:00 weight: 7
Night Index (`content/night/_index.md`)
title: "Go 源码阅读" date: 2018-11-15T12:32:37+08:00 weight: 2
Other Index (`content/other/_index.md`)
title: "其他" date: 2018-11-15T12:32:37+08:00 weight: 10
Proposal Index (`content/proposal/_index.md`)
title: "建议" date: 2018-11-15T12:32:37+08:00 weight: 9
Articles Sonarqube For Golang Index (`content/articles/sonarqube-for-golang/index.md`)
title: Golang 代码质量持续检测 date: 2018-07-22T00:00:00+08:00
Author: Kenny Allen
Email: [email protected]
前言
在软件开发过程中,人工检查项目代码中的漏洞和潜在的 BUG 是一件让人十分费神费力的事情,为了解决这一痛点,SonarQube诞生了,它实现了一系列代码自动检测过程,包括命名规范,代码漏洞,代码重复量等。
但是光有 SonarQube 还不能发挥出应有的高效率,一个完整的代码质量持续检测应配合代码仓库(如 gitlab) 和 Jenkins 来共同构建一个自动化过程。
环境
Gitlab、Jenkins、SonarQube 服务都在一台物理机上的Docker中运行
网络 (局域网IP:192.168.1.100)
主机科学上网代理
192.168.1.100:1087
模拟外网访问
shell# 修改 hosts 文件,模拟外网访问 sudo sh -c "echo '192.168.1.100 jenkins.kenny.com\n192.168.1.100 gitlab.kenny.com\n192.168.1.100 sonarqube.kenny.com' >> /etc/hosts"工具集
| 名称 | 版本 |
|---|---|
| golang | go1.10.3 |
| docker | 18.03.1-ce |
搭建
接下来,我以一个完整的案例来介绍搭建过程。
Jenkins
启动服务
# $jenkins_home 宿主机目录,挂载容器 /var/jenkins_home
# 我的数据卷目录是 ~/.jenkins
export JENKINS_HOME=~/.jenkins
docker run -d --restart=always -p 8080:8080 -p 50000:50000 -v $JENKINS_HOME:/var/jenkins_home --name jenkins jenkins:2.60.3
# 查看 jenkins 日志
docker logs -f jenkins初始化
打开浏览器,访问 http://jenkins.kenny.com:8080
shell# 在日志中找到管理员密码 docker logs -f jenkins # 或者在 $JENKINS_HOME/secrets/initialAdminPassword 文件中找到管理员密码 cat $JENKINS_HOME/secrets/initialAdminPassword安装推荐插件 (如果你想自定义安装插件,点击 Select plugins to Install)
创建管理员账号
初始化完成
Gitlab
启动服务 (gitlab 集成的服务比较多,因此需要占用较大的内存,官方推荐4GB以上)
# $gitlab_home 宿主机目录
# 我的数据卷目录是 ~/.gitlab
export GITLAB_HOME=~/.gitlab
docker run -d --restart=always -e 'GITLAB_HOST=gitlab.kenny.com' -p 443:443 -p 80:80 -p 22:22 -v $GITLAB_HOME/conf:/etc/gitlab -v $GITLAB_HOME:/var/opt/gitlab -v $GITLAB_HOME/log:/var/log/gitlab --name gitlab gitlab/gitlab-ce:11.1.0-ce.0
# 查看 gitlab 日志
docker logs -f gitlab初始化
打开浏览器,访问 http://gitlab.kenny.com
设置新密码后,使用 root 用户登录
新建 sonarqube 项目组
在 sonarqube 项目组下新建 demo 项目
添加主机公钥到 Gitlab
shell# 生成 rsa 公钥和密钥 ssh-keygen -t rsa # 查看并复制公钥 cat ~/.ssh/id_rsa.pub访问 http://gitlab.kenny.com/profile/keys ,将公钥添加至 Gitlab
将 sonarqube/demo 项目拉至主机的 $GOPATH 下
shell# 在 $GOPATH 下创建 gitlab.kenny.com 文件夹 mkdir -p $GOPATH/src/gitlab.kenny.com && cd $GOPATH/src/gitlab.kenny.com # clone code git clone [email protected]:sonarqube/demo.git
SonarQube
启动服务
# 由于目前 sonarqube 官方的 Docker images 只有 7.1 版本,不满足 SonarGO 所需 7.2+ 版本,所以我参考7.1 的 Dockerfile 制作了一个 sonarqube 7.2.1 的镜像
# $sonarqube_home 宿主机目录
# 我的数据卷目录是 ~/.sonarqube
export SONARQUBE_HOME=~/.sonarqube
# 正式环境中应启用外部数据库服务来存储必要数据,在启动容器时设置如下JDBC相关参数:
# -e SONARQUBE_JDBC_USERNAME=sonar
# -e SONARQUBE_JDBC_PASSWORD=sonar
# -e SONARQUBE_JDBC_URL=jdbc:postgresql://localhost/sonar
docker run -d --restart=always -p 9000:9000 -v $SONARQUBE_HOME:/opt/sonarqube/data --name sonarqube kennyallen/sonarqube:7.2.1
# 查看 sonarqube 日志
docker logs -f sonarqube文件:Dockerfile、run.sh
初始化
打开浏览器,访问 http://sonarqube.kenny.com:9000
使用管理员账号登录
- 账号 admin
- 密码 admin
生成 token (作为远程连接 SonarQube 的标识,只生成一次,记得备份哦)
admin_token: 74439d5bc557dcc206fa8b1f2f5516e65680bdc8
安装插件 (进入 Administration -> Marketplace)
安装完成后点击重启 SonarQube 服务就OK了
集成
- 将 Jenkins、Gitlab 和 SonarQube 有机整合
Jenkins 安装插件
点击进入 系统管理 -> 插件管理 -> 可选插件
过滤选中 Gitlab、SonarQube Scanner,点击下载待重启后安装
Jenkins 配置
安装 SonarQube & JDK
进入 系统管理 -> Global Tool Configuration
JDK 安装
勾选我同意 Java SE Development Kit 的许可协议
点击 Please enter your username/password
输入你的 oracle 账号密码
SonarQube Scanner 安装
点击保存。
SonarQube Server
进入 系统管理 -> 系统设置
Add SonarQube servers
Name 随便填写
Server URL: http://sonarqube.kenny.com:9000
Server version: 5.3 or higher
Server authentication token: 填 SonarQube 初始化时生成的 token
取消 Gitlab 授权
取消选中 Enable authentication for '/project' end-point,保存
在 jenkins 容器中安装 golang 环境及工具
shell# 在 Jenkins 容器中执行命令 docker exec -it jenkins /bin/bash # 临时设置环境变量 export GOROOT=$JENKINS_HOME/go export GOPATH=$JENKINS_HOME/workspace/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin export http_proxy=http://192.168.1.100:1087;export https_proxy=http://192.168.1.100:1087; # 进入 jenkins 主目录 cd $JENKINS_HOME # 下载 golang wget https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz # 解压 golang 包 tar -xvf go1.10.3.linux-amd64.tar.gz # 删除 golang 包 rm go1.10.3.linux-amd64.tar.gz # 安装必要工具 # vgo go get -u -v golang.org/x/vgo # gometalinter go get -u -v github.com/alecthomas/gometalinter gometalinter --install配置邮件通知
进入 系统管理 -> 系统设置
Jenkins Location
系统管理员邮件地址修改为你自己的邮箱地址,如 [email protected]
Extend E-mail Notification
SMTP Server 填写对应的SMTP服务地址,如 smtp.163.com
点击高级,勾选使用SMTP认证
用户名 注意不需要加 @xxx.xxx
密码 填写自己的邮箱密码或授权码
Default Triggers 选中 Always
新建 Jenkins 构建任务
新构建一个自由风格的软件项目
使用自定义的工作空间
点击高级,勾选使用自定义的工作空间
目录:$JENKINS_HOME/workspace/go/src/gitlab.kenny.com/demo
源码管理
Repository URL:http://gitlab.kenny.com/sonarqube/demo
Credentials:点击Add ,添加凭据 (Gitlab 用户名密码或 SSH登录等方式都可以)
构建触发器,选中
Build when a change is pushed to GitLab. GitLab webhook URL: http://jenkins.kenny.com:8080/project/demo
Enabled GitLab triggers 选中 Push Events 和 Accepted Merge Request Events ,表示当 Gitlab 有 push 或 merge 操作发生时触发构建。
新建 webhook
在浏览器中打开 http://gitlab.kenny.com/admin/application_settings (请使用 root 登录),找到 Outbound requests ,点击 Expand 后,选中 Allow requests to the local network from hooks and services 并保存更改。 (允许本地网络的 githook)
进入 http://gitlab.kenny.com/sonarqube/demo/settings/integrations
URL: http://jenkins.kenny.com:8080/project/demo
SecretToken: 不填
选中 Push events、Merge request events
取消选中 Enable SSL verification
点击 Add web hook
增加构建步骤,选中 Execute Shell
shell#!/bin/bash # 环境变量 export GOROOT=$JENKINS_HOME/go export GOPATH=$JENKINS_HOME/workspace/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin export http_proxy=http://192.168.1.100:1087;export https_proxy=http://192.168.1.100:1087; # 安装依赖 vgo mod -vendor # coverage go test ./... -coverprofile=coverage.out # test go test ./... -json > report.json # vet go vet ./... 2> govet-report.out # golint golint ./... > golint-report.out # gometalinter # 执行 gometalinter 会失败,因此加了 || true gometalinter ./... > gometalinter-report.out || true增加构建步骤,选中 Execute SonarQube Scanner
Analysis properties
propertiessonar.projectKey=gitlab.kenny.com sonar.projectName=demo sonar.sources=. sonar.exclusions=**/*_test.go,**/vendor/** sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/** sonar.go.coverage.reportPaths=coverage.out sonar.go.tests.reportPaths=report.json sonar.go.govet.reportPaths=govet-report.out sonar.go.golint.reportPaths=golint-report.out sonar.go.gometalinter.reportPaths=gometalinter-report.out增加构建后操作,选中 Editable Email Notification
Project Recipient List 填写接收邮件的Email地址,或使用默认配置
Default Content 加上 SonarQube URL: http://sonarqube.kenny.com:9000
测试
# clone demo 代码
cd $GOPATH/src/gitlab.kenny.com && git clone [email protected]:yuhao5/sonarqube-golang.git && rm -rf demo && mv sonarqube-golang demo && cd demo
# push 代码,触发 Jenkins 任务进行自动构建
git remote add gitlab [email protected]:sonarqube/demo.git
git push -u gitlab master
# 若 gitlab 仓库地址不是 [email protected]:sonarqube/demo.git ,请根据以下步骤修改:
docker exec -it gitlab /bin/bash
vim /etc/gitlab/gitlab.rb
# 找到 external_url,修改为 external_url 'http://gitlab.kenny.com'
# 然后执行
gitlab-ctl reconfigureTODO
- 解决执行 gometalinter 失败问题
- Golang 质量标准,规则自定义
- ...
Home (`content/home.md`)
date: 2018-11-08T21:07:13+01:00 last_date: 2020-03-05T15:08:00+08:00 title: Go 夜读 type: index weight: 0
加入方法
微信
微信搜索 night_reading_go ,添加好友,备注你的姓名、公司、工作岗位和职责,
来自:Github,我会拉你入群。
订阅 Go 夜读微信公众号:
YouTube, Twitter, Facebook, Telegram, Slack
我们的精神
开源!开源!开源!
重要的事,一定要说三遍。
希望有兴趣的小伙伴们一起加入,让我们一起把 『Go 夜读』建立成一个对大家都有帮助的开源社区。
我们的目标
我们希望可以推进大家深入了解 Go ,快速成长为资深的 Gopher 。
我们希望每次来了的人和没来的人都能够有收获,成长。
让每个想要学习的人都能参与进来,(包括初中高级 Go 工程师),
只有层次相当的人才有可能有思维的碰撞和交流,这样最终的产出也尽可能的高质量。
主题内容
Go 夜读将定期进行与 Go 语言相关的话题分享,例如源码阅读、工程实践等等,你如果是 Go 新手可以先去这里查看 Go 学习之路。
我们的选题范围
我们的选题范围包括但不限于:
- 入门级
- 实操级
- 架构设计级
- 学习方法、习惯培养等
- 效率效能提升
- 论文研讨
我们的基本流程和分享方式
- 通过提交 Issue 的方式来收集大家想要研究的与 Go 相关的源码库或源码模块等话题;
- 提交的话题提案必须得到得到 SIG 小组的批准,并成功招募到分享人,该分享才会进入准备阶段。同样欢迎自荐话题并主动进行分享;
- 分享人准备分享材料,并在材料准备完毕后交付 SIG 小组审阅;
- 当 SIG 小组完成对材料的审阅后,将进行正式排期(这期间包括划定受众范围、审阅任务分工、分发排期计划等);
- 正式在线上进行分享;
- 将视频进行后期剪辑并上传至视频网站,再进行后续分发。
回看地址
往期分享
见 GitHub 项目主页。
Go 夜读 SIG 小组
SIG 的全称是 Special Interests Group, 或称 Super Intellectual Genius。
Go 夜读 SIG 小组负责 Go 夜读活动的日常维护,目前的核心成员包括:
想要加入?参考如何加入 Go 夜读 SIG 小组。
如何发起分享提案?
你是否经常困扰于某些 Go 话题没有人分享或者很少人关注?自己很想深入研究,但是却是形单影只,经常半途而废呢?
机会来了!!!参考如何发起分享提案
如何参与贡献?
想要参与贡献?阅读 如何参与贡献 查看指南。
Stargazers over time
night-reading-go Star History and Stats
Contributors
我非常重视每一个对这个项目的贡献者,我会将贡献者列表更新到这里,目前只有提交 Pull Request 的小伙伴,但是贡献不仅仅如此,还可以包括提交 Issue 以及在社群中有所贡献的人。
贡献者自己可以提 PR ,方法如下:
- 安装
npm install --save-dev all-contributors-cli sh gen_contributors.sh
贡献类型有多种,比如:"code", "ideas","review","talk","tutorial",你可以在 .all-contributorsrc 中修改。
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
Algorithms 2020 12 27 (`content/algorithms/2020-12-27.md`)
title: 2020-12-27 TalkGo 算法之美微信群今日讨论问题 date: 2020-12-27T22:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2020-12-27
算法之美微信群当日讨论列表
- Leetcode双周赛第四题:得到连续 K 个 1 的最少相邻交换次数
原题链接:https://leetcode-cn.com/problems/minimum-adjacent-swaps-for-k-consecutive-ones/ - Q30 用插线板制作章鱼脚状线路
原题链接:https://leetcode-cn.com/leetbook/read/interesting-algorithm-puzzles-for-programmers/9l2pjv/
TalkGo 算法之美专栏更新
Algorithms 2020 12 28 (`content/algorithms/2020-12-28.md`)
title: 2020-12-28 TalkGo 算法之美第三期第一周第一阶段 date: 2020-12-28T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2020-12-28
周一题目的链接:https://talkgo.org/t/topic/1443
主题:树的构造
先重温下二叉树是怎么构造出来的吧
附加题:
- Tips: 递归
Algorithms 2020 12 29 (`content/algorithms/2020-12-29.md`)
title: 2020-12-29 TalkGo 算法之美第三期第一周第二阶段 date: 2020-12-29T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2020-12-29
2020.12.29
TalkGo算法之美群今日讨论问题将有序数组转换为二叉搜索树
题目链接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/
题解链接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/solution/108-jiang-you-xu-shu-zu-zhuan-huan-wei-e-muuy/(来源:木头(算法之美群内成员))不同的二叉搜索树 II
原题链接:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/
题解链接:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/solution/95-bu-tong-de-er-cha-sou-suo-shu-ii-di-g-qu38/(来源:木头(算法之美群内成员))
关于如何建设好算法之美群:
吴俊伟:我觉得可以分两类人,一种是为了找工作而刷题的,一种是单纯为了学习算法。对于前一种可以多找些高频面试题,后一种就按照现在来就好了。提高参与度的话就让大家可以把自己觉得好的题目或者做不出来的题目,又或者是面试遇到的题目发出来让大家看看,一起讨论。
吴俊伟:还有平台可以不用局限leetcode,洛谷,计蒜客,牛客网,poj都行的
小林:可以发布一些算法比赛消息
吴俊伟:不过我们也可以照顾到这些人多发些面试常考的"经典"题
Algorithms 2020 12 30 (`content/algorithms/2020-12-30.md`)
title: 2020-12-30 TalkGo 算法之美第三期第一周第二阶段 date: 2020-12-30T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2020-12-30
- 2020.12.30
TalkGo算法之美群今日讨论问题- 算法之美第三期周三题目
原题链接以及群友题解(题解见评论):https://talkgo.org/t/topic/1443 - 比赛消息
由群成员吴俊伟提供
牛客2020跨年场
比赛时间:2020-12-31 21:00:00 - 2021-01-01 00:30:00
比赛地址:https://ac.nowcoder.com/acm/contest/9854
- 算法之美第三期周三题目
Algorithms 2021 01 01 (`content/algorithms/2021-01-01.md`)
title: 2021-01-01 TalkGo 算法之美第三期第一周第三阶段
date: 2021-01-01T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-01
2021.1.1
TalkGo算法之美群今日讨论要素- 算法之美第三期第一周周五题目链接:https://talkgo.org/t/topic/1443
- leetcode435. 无重叠区间 (由群友木头提供)
原题链接:https://leetcode-cn.com/problems/non-overlapping-intervals/ - 群友roseduan分享刷题记录
链接:https://github.com/roseduan/algo-learn - 本周线上讨论会预定于明天(2020.1.2)下午两点举行,欢迎各位来参与讨论。
Algorithms 2021 01 03 (`content/algorithms/2021-01-03.md`)
title: 2021-01-03 TalkGo 算法之美第三期第一周第三阶段 date: 2021-01-03T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-03
- 2021.1.3
TalkGo算法之美群今日讨论要素
leetcode周赛:https://leetcode-cn.com/contest/weekly-contest-222/
leetcode每日一题: 86. 分隔链表
原题链接:https://leetcode-cn.com/problems/partition-list/
Algorithms 2021 01 06 (`content/algorithms/2021-01-06.md`)
title: 2021-01-06 TalkGo 算法之美第三期第二周第三阶段 date: 2021-01-06T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-06
- 2021.01.06
- TalkGo算法之美第三期第二周开始啦。
以下是周三题目的链接:https://talkgo.org/t/topic/1502
主题:二叉搜索树
Algorithms 2021 01 08 (`content/algorithms/2021-01-08.md`)
title: 2021-01-08 TalkGo 算法之美第三期第二周第三阶段 date: 2021-01-08T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-08
- 2021.01.08
- TalkGo算法之美第三期第二周开始啦。
以下是周五题目的链接:https://talkgo.org/t/topic/1502
主题:巧用遍历
Algorithms 2021 01 09 (`content/algorithms/2021-01-09.md`)
title: 2021-01-09 TalkGo 算法之美第三期第三周第二阶段 date: 2021-01-09T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-9
- 2021.10.9
- TalkGo算法之美群今日讨论要素
- leecode每日一题:123. 买卖股票的最佳时机 III
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
- leecode每日一题:123. 买卖股票的最佳时机 III
Algorithms 2021 01 11 (`content/algorithms/2021-01-11.md`)
title: 2021-01-11 TalkGo 算法之美第三期第三周第一阶段 date: 2021-01-11T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-11
- 2021.1.11
- TalkGo算法之美第三期第三周开始啦。
以下是周一题目的链接:https://talkgo.org/t/topic/1550
主题:回溯
Algorithms 2021 01 12 (`content/algorithms/2021-01-12.md`)
title: 2021-01-12 TalkGo 算法之美第三期第三周第二阶段 date: 2021-01-12T08:31:00+08:00
来源:『TalkGo 算法之美』微信群
时间:2021-01-12
2021.1.12
TalkGo算法之美群今日讨论要素百度面试题(群友bill提供):https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/
腾讯面试题(群友小林提供):https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/
面试高频题(群友bill提供):https://leetcode-cn.top/#/home
Articles 2018 05 31 Batch Del Redis Key (`content/articles/2018-05-31-batch-del-redis-key.md`)
title: 批量删除redis中的key date: 2018-05-31T00:00:00+08:00
- 1.首先看图
- 1.我创建了三个以
go:read开头的key- 2.通过
keys go:read:*可以全部找出来- 3.接下来退出redis-cli,使用
redis-cli -p 6379 keys "go:read*" | xargs redis-cli -p 6379 del可以批量删除
- 2.查看原理
- 1.执行
redis-cli -p 6379 keys "go:read*"控制台输出了需要的key- 2.执行
redis-cli -p 6379 keys "go:read*" | xargs -0 echo,可以看到输出了需要的key,但是有点不一样,双引号被去掉了,这说明数据通过管道传递给了xargs作了相应处理- 3.执行
redis-cli -p 6379 keys "go:read*" | xargs redis-cli -p 6379 del删除了输出的key,这说明xargs对接收到数据分别进行了redis-cli -p 6379 del操作
- 3.遇到的问题
- 1.同样的原理,我设置了三个key,但是这三个key里面包含了双引号,双引号前得加上\转义符
- 2.使用
redis-cli -p 6379 keys "go:read*" | xargs redis-cli -p 6379 del,发现没删除- 3.通过
redis-cli -p 6379 keys "go:read*" | xargs -0 echo,发现传输到xargs时把\转义符删掉了,去执行redis del操作的时候key不配,导致删除失败- TODO 采用xargs我没找到解决方案,希望各位大神求助,有方法了可以写在下面@~@
- 4.另一种解决方案
for i in $(redis-cli -p 6379 keys "go:read:*");do redis-cli -p 6379 del "$i";done- 采用shell脚本格式 -> for循环读取key去删除
Articles 2018 11 11 Golang File Lock (`content/articles/2018-11-11-golang-file-lock.md`)
title: golang 的文件锁操作 date: 2018-11-11T00:00:00+08:00
这篇文章给大家介绍一下 golang 的文件锁。我们在使用 golang 开发程序的时候,经常会出现多个 goroutine 操作同一个文件(或目录)的时候,如果不加锁,很容易导致文件中的数据混乱,于是,Flock 应运而生。
Flock 是对于整个文件的建议性锁(不强求 goroutine 遵守),如果一个 goroutine 在文件上获取了锁,那么其他 goroutine 是可以知道的。默认情况下,当一个 goroutine 将文件锁住,另外一个 goroutine 可以直接操作被锁住的文件,原因在于 Flock 只是用于检测文件是否被加锁,针对文件已经被加锁,另一个 goroutine 写入数据的情况,内核不会阻止这个 goroutine 的写入操作,也就是建议性锁的内核处理策略。
函数
import "syscall"
func Flock(fd int, how int) (err error)Flock 位于 syscall 包中,fd 参数指代文件描述符,how 参数指代锁的操作类型。
how 主要的参数类型:
- LOCK_SH,共享锁,多个进程可以使用同一把锁,常被用作读共享锁;
- LOCK_EX,排他锁,同时只允许一个进程使用,常被用作写锁;
- LOCK_NB,遇到锁的表现,当采用排他锁的时候,默认 goroutine 会被阻塞等待锁被释放,采用 LOCK_NB 参数,可以让 goroutine 返回 Error;
- LOCK_UN,释放锁;
示例
下面的例子来自于 NSQ,位于 nsq/internal/dirlock,用于实现对目录的加锁
// +build !windows
package dirlock
import (
"fmt"
"os"
"syscall"
)
// 定义一个 DirLock 的struct
type DirLock struct {
dir string // 目录路径,例如 /home/XXX/go/src
f *os.File // 文件描述符
}
// 新建一个 DirLock
func New(dir string) *DirLock {
return &DirLock{
dir: dir,
}
}
// 加锁操作
func (l *DirLock) Lock() error {
f, err := os.Open(l.dir) // 获取文件描述符
if err != nil {
return err
}
l.f = f
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) // 加上排他锁,当遇到文件加锁的情况直接返回 Error
if err != nil {
return fmt.Errorf("cannot flock directory %s - %s", l.dir, err)
}
return nil
}
// 解锁操作
func (l *DirLock) Unlock() error {
defer l.f.Close() // close 掉文件描述符
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) // 释放 Flock 文件锁
}总结
- Flock 是建议性的锁,使用的时候需要指定
how参数,否则容易出现多个 goroutine 共用文件的问题 how参数指定LOCK_NB之后,goroutine 遇到已加锁的 Flock,不会阻塞,而是直接返回错误
Articles 2019 11 07 Raft 易于理解的一致性算法 (`content/articles/2019-11-07-Raft--易于理解的一致性算法.md`)
title: Raft--易于理解的一致性算法 date: 2019-11-07T00:00:00+23:00
这篇文章讲解如何用 go 实现 raft 算法,代码框架来自于 Mit6.824分布式课程
最初,为了学习分布式系统,我了解到了 Mit 6.824课程,课程的 lab 需要用 go 来完成。于是 go 走进了我的世界,go 很容易入门, 写起来很舒服,但是要真正的理解 go, 并不是很容易, 特别是对 goroutine, select, channel 等的灵活运用。俗话说的好,初生牛犊不怕虎, 在初步了解 go 之后, 我就开始学习课程了。 每个 lab 都会有对应的论文, 比如 mapreduce, raft 等等。 lab2 是实现 raft 算法, lab3 是基于 lab2的 raft 算法来 实现一个简单的分布式 kv 存储。 在做 lab 的过程中,不仅仅可以对 raft 的细节有更好的把握, 同时对 go 语言的理解也会逐渐加深, 特别是并发部分。
首先看一下复制状态机,如下图所示
复制状态机通常是基于复制日志实现的。每一台服务器存储着一个包含一系列指令的日志,每个日志都按照相同的顺序包含相同的指令,所以每一台服务器都执行相同的指令序列。那么如何保证每台服务器上的日志都相同呢? 这就是接下来要介绍的一致性算法raft要做的事情了。
raft 主要分三大部分, 领导选举, 日志复制, 日志压缩。 由于其中的细节很多,所以在实现过程中肯定会遇到各种各样的问题, 这也是一个比较好的事情,因为问题将促使我们不断地深入的去阅读论文, 同时锻炼 debug 并发程序的能力。最后肯定是满满的收获。
实现主要依赖于raft论文中的下图
代码框架条理清楚。主要包含七个主要的 struct , 三个 RPC handler , 四个主干函数 , 如下
/* Detailed source-code truncated for AI context efficiency. */下面的部分, 记录了三大部分的主干和我认为的容易出错的地方。
领导选举
1. electForLeader 函数主干,候选人针对每一个 peer 发送请求投票RPC
for i:=0; i<len(rf.peers); i++ {
// meet myself
if i==rf.me {
continue
}
go func(index int) {
reply := &RequestVoteReply{}
response := rf.sendRequestVote(index, &args, reply)
if response {
...
}
}(i)
}2. 获得响应后,要将 reply 的 term 于 candidate 的 currentTerm 进行比较
if reply.Term > rf.currentTerm {
rf.currentTerm = reply.Term
rf.changeRole("follower")
return
}3. 获得响应后,候选人要检查自己的 state 和 term 是否因为发送RPC而改变
if rf.state != "candidate" || rf.currentTerm!= args.Term { return }4. 若候选人获得的投票超过半数,则变成领导人
5. 请求投票PRC ⭐(接收者指接收 请求投票PRC 的 peer)
如果 candidate 的 term 小于接收者的 currentTerm, 则不投票,并且返回接收者的 currentTerm
goreply.VoteGranted = false reply.Term = rf.currentTerm if rf.currentTerm > args.Term { return }如果接收者的 votedFor 为空或者为 candidateId,并且 candidate 的日志至少和接收者一样新,那么就投票给候选人。candidate 的日志至少和接收者一样新的含义:candidate 的最后一个日志条目的 term 大于接收者的最后一个日志条目的 term 或者当二者相等时,candidate 的最后一个日志条目的 index 要大于等于接收者的
goif (rf.votedFor==-1 || rf.votedFor==args.CandidateId) && (args.LastLogTerm > rf.getLastLogTerm() || ((args.LastLogTerm==rf.getLastLogTerm())&& (args.LastLogIndex>=rf.getLastLogIndex()))) { reply.VoteGranted = true rf.votedFor = args.CandidateId rf.state = "follower" // rf.state can be follower or candidate ... }
日志复制
1. appendLogEntries 函数的主干,leader 针对每一个 peer 发送附加日志 RPC
for i:=0; i<len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(index int) {
reply := &AppendEntriesReply{}
respond := rf.sendAppendEntries(index, &args, reply)
if reply.Success {
...
return
} else {
...
}
}(i)
}2. 获得响应后,要将 reply 的 term 于 leader 的 currentTerm 进行比较
3. 获得响应后,候选人要检查自己的 state 和 term 是否因为发送RPC 而改变
4. 回复成功
更新 nextIndex, 即 leader 需要发送给该 peer 下一条日志条目的索引值 , 更新 matchIndex, 即 leader 已经复制给该 peer 的日志的最高索引值
gorf.matchIndex[index] = args.PrevLogIndex + len(args.Entries) rf.nextIndex[index] = rf.matchIndex[index] + 1如果存在一个 N 满足 N>commitIndex,并且大多数 matchIndex[i] > N 成立,并且 log[N].term == currentTerm,则更新 commitIndex=N
5. 回复不成功
- 更新 nextIndex,然后重试
6. 附加日志RPC ⭐
几个再次明确的地方:
preLogIndex 的含义:新的日志条目(s)紧随之前的索引值,是针对每一个follower而言的==nextIndex[i]-1,每一轮重试都会改变。
entries[] 的含义:准备存储的日志条目;表示心跳时为空
goappend(make([]LogEntry, 0), rf.log[rf.nextIndex[index]-rf.LastIncludedIndex:]...)
领导人获得权力后,初始化所有的 nextIndex 值为自己的最后一条日志的 index+1;如果一个 follower 的日志跟领导人的不一样,那么在附加日志 PRC 时的一致性检查就会失败。领导人选举成功后跟随者可能的情况
reply增加 ConflictIndex 和 ConflictTerm 用于记录日志冲突 index 和 term
如果 leader 的 term 小于接收者的 currentTerm, 则 reply false.
接下来就三种情况 ⭐(比较绕且容易出错)
- follower 的日志长度比 leader 的短, 那么 ConflictIndex 就是 follower 的 日志长度。之后在 appendLogEntries 函数里面会更新 nextIndex 为 ConflictIndex, 相应的 prevLogIndex 也会改变, 于是在下一轮RPC中便可以比较 follower 的日志条目在 arg.PrevLogIndex 索引处的 Term 是否等于 args.PrevLogTerm, 如果相等, 则该可以把 leader 在 arg.PrevLogIndex 后面的一次性全部添加到 follower 的日志条目上, 从而达成一致性, 否则令 ConflictTerm 为 follower 的日志条目在 arg.PrevLogIndex 索引处的 Term, 然后从头遍历 follower 的日志条目, 找到第一个 term 等于 ConflictTerm 的 索引, 用来更新 ConflictIndex
- follower 的日志长度比 leader 的长,且在 prevLogIndex 处的 term 相等, 则开始依次对比 follower 在 arg.prevLogIndex 后的日志条目在何处跟 entries 上相应的日志的 term 不一致, 假设该索引为 idx, 那么 follower 在索引后的日志条目应该被替换为 entries 在 该索引后的日志条目, 最终达成一致性
- follower 的日志长度比 leader 的长,且在 prevLogIndex 处的 term 不相等, 之后便是去寻找 ConflictTerm 然后更新 ConflictIndex, 直到在 prevLogIndex 处的 term 相等, 然后按照 2 进行处理.
goif args.PrevLogIndex >=rf.LastIncludedIndex && args.PrevLogIndex < rf.logLen() { if args.PrevLogTerm != rf.log[args.PrevLogIndex-rf.LastIncludedIndex].Term { reply.ConflictTerm = rf.log[args.PrevLogIndex-rf.LastIncludedIndex].Term // then search its log for the first index // whose entry has term equal to conflictTerm. for i:=rf.LastIncludedIndex; i<rf.logLen(); i++ { if rf.log[i-rf.LastIncludedIndex].Term==reply.ConflictTerm { reply.ConflictIndex = i break } } return } }else { reply.ConflictIndex = rf.logLen() return } index := args.PrevLogIndex for i:=0; i<len(args.Entries); i++ { index++ if index >= rf.logLen() { rf.log = append(rf.log, args.Entries[i:]...) rf.persist() break } if rf.log[index-rf.LastIncludedIndex].Term != args.Entries[i].Term { rf.log = rf.log[:index-rf.LastIncludedIndex] rf.log = append(rf.log, args.Entries[i:]...) rf.persist() break } }如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和新日志条目索引值中较小的一个
日志压缩
1. 采用日志压缩,不仅可以减少本地占用的空间和每次重置的时间花销, 还可以直接通过网络把快照发给那些落后的跟随者,使他们更新到最新的状态
2. 下图为 Raft 中快照的基本思想, 快照中包含状态机的状态, LastIncludeIndex: 被快照取代的最后的条目在日志中的索引值, LastIncludedTerm: 该条目的任期号,保留这些数据是为了支持快照后紧接着的第一个条目的附加日志请求时的一致性检查
3. 安装快照RPC
尽管服务器通常都是独立的创建快照,但是领导人必须偶尔的发送快照给一些落后的跟随者
三种情况
- leader 的 LastIncludedIndex 小于等于 follower 的 LastIncludeIndex
- leader 的 LastIncludedIndex 大于 follower 的 LastIncludeIndex,leader 的 LastIncludedIndex 小于 follower 日志的最大索引值
- leader 的 LastIncludedIndex 大于等于 follower 日志的最大索引值
对应的处理方式
- 如果接收到的快照是自己日志的前面部分,那么快照包含的条目将全部被删除,但是快照后面的条目仍然有效,要保留
- 如果快照中包含没有在接收者日志中存在的信息,那么跟随者丢弃其整个日志,全部被快照取代。
总结
1. 个人认为看一两遍论文就掌握 raft 还是比较困难的, 所以当对 raft 有了大致了解之后, 就可以开始实现了, 遇到问题后带着问题去读, 反复咀嚼, 逐渐加深对 raft 的理解
2. 推荐看一下这个: raft 可视化, 可以对 raft 有一个直观的感受
Articles 2020 02 08 Source Go Flag (`content/articles/2020-02-08-source_go_flag.md`)
title: "Go 源码阅读之 flag 包" date: 2020-02-08T17:54:04+08:00
简介
flag 包是 Go 里用于解析命令行参数的包。为什么选择它作为第一个阅读的包,因为它的代码量少。其核心代码只有一个 1000 不到的 flag.go 文件。
文件结构
flag 包的文件结构很简单,就一层。一个文件夹里放了 5 个文件,其文件及其作用如下:
flag.go
flag 的核心包,实现了命令行参数解析的所有功能
export_test.go
测试的实用工具,定义了所有测试需要的基础变量和函数
flag_test.go
flag 的测试文件,包含了 17 个测试单元
example_test.go
flag 的样例文件,介绍了 flag 包的三种常用的用法样例
example_value_test.go
flag 的样例文件,介绍了一个更复杂的样例
运行测试
我先介绍一下 Go 的运行环境。
# 通过 brew install go 安装,源码位置为 $GOROOT/src
GOROOT=/usr/local/opt/go/libexec
# 阅读的源码通过 go get -v -d github.com/haojunyu/go 下载,源码位置为 $GOPATH/src/github.com
GOPATH=$HOME/go单独测试 flag 包踩过的坑:
- 无法针对单个文件进行测试,需要针对包。
这里重点说一下 export_test.go 文件,它是flag包的一部分package flag,但是它确实专门为测试而存在的,说白了也就一个ResetForTesting方法,用来清除所有命令参数状态并且直接设置Usage函数。该方法会在测试用例中被频繁使用。所以单独运行以下命令会报错"flag_test.go:30:2: undefined: ResetForTesting"
# 测试当前目录(报错)
go test -v .
# 测试包
go test -v flaggo test -v flag测试的源码是$GOROOT/src下的(以我当前的测试环境)
指定 flag 包后,实际运行的源码是 $GOROOT 下的,这个应该和我的安装方式有关系。
总结
接口转换能实现类似 C++ 中模板的功能
flag 包中定义了一个结构体类型叫 Flag,它用来存放一个命令参数,其定义如下。
// A Flag represents the state of a flag.
// 结构体Flag表示一个参数的所有信息,包括名称,帮助信息,实际值和默认值
type Flag struct {
Name string // name as it appears on command line名称
Usage string // help message帮助信息
Value Value // value as set实现了取值/赋值方法的接口
DefValue string // default value (as text); for usage message默认值
}其中命令参数的值是一个 Value 接口类型,其定义如下:
// Set is called once, in command line order, for each flag present.
// The flag package may call the String method with a zero-valued receiver,
// such as a nil pointer.
// 接口Value是个接口,在结构体Flag中用来存储每个参数的动态值(参数类型格式各样)
type Value interface {
String() string // 取值方法
Set(string) error // 赋值方法
}为什么这么做?因为这样做能够实现类似模板的功能。任何一个类型 T 只要实现了 Value 接口里的 String 和 Set 方法,那么该类型 T 的变量 v 就可以转换成 Value 接口类型,并使用 String 来取值,使用 Set 来赋值。这样就能完美的解决不同类型使用相同的代码操作目的,和 C++ 中的模板有相同的功效。
函数 vs 方法
函数和方法都是一组一起执行一个任务的语句,二者的区别在于调用者不同,函数的调用者是包 package,而方法的调用者是接受者 receiver。在 flag 的源码中,有太多的函数里面只有一行,就是用包里的变量 CommandLine 调用同名方法。
// Parsed reports whether f.Parse has been called.
// Parsed方法: 命令行参数是否已经解析
func (f *FlagSet) Parsed() bool {
return f.parsed
}
// Parsed reports whether the command-line flags have been parsed.
func Parsed() bool {
return CommandLine.Parsed()
}`new` vs `make`
new 和 make 是 Go 语言中两种内存分配原语。二者所做的事情和针对的类型都不一样。new 和其他编程语言中的关键字功能类似,都是向系统申请一段内存空间来存储对应类型的数据,但又有些区别,区别在于它会将该片空间置零。也就是说 new(T) 会根据类型 T 在堆上 申请一片置零的内存空间,并返回指针 *T。make 只针对切片,映射和信道三种数据类型 T 的构建,并返回类型为 T 的一个已经初始化(而非零)的值。原因是这三种数据类型都是引用数据类型,在使用前必须初始化。就像切片是一个具有三项内容的描述符,包含一个指向数组的指针,长度和容量。通过 make 创建对应类型的变量过程是先分配一段空间,接着根据对应的描述符来创建对应的类型变量。关于 make 的细节可以看 draveness 写的 Go语言设计与实现。
// Bool defines a bool flag with specified name, default value, and usage string.
// The return value is the address of a bool variable that stores the value of the flag.
func (f *FlagSet) Bool(name string, value bool, usage string) *bool {
p := new(bool)
f.BoolVar(p, name, value, usage)
return p
}
// sortFlags returns the flags as a slice in lexicographical sorted order.
// sortFlags函数:按字典顺序排序命令参数,并返回Flag的切片
func sortFlags(flags map[string]*Flag) []*Flag {
result := make([]*Flag, len(flags))
i := 0
for _, f := range flags {
result[i] = f
i++
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result
}指针赋值给接口变量
Go 中的接口有两层含义,第一层是一组方法(不是函数)的签名,它需要接受者(具体类型 T 或具体类型指针 *T )来实现细节;另一层是一个类型,而该类型能接受所有现实该接受的接受者。深入理解接口的概念可以细读 Go语言设计与实现之接口。在 flag 包中的 StringVar 方法中newStringValue(value, p)返回的是 *stringValue 类型,而该类型(接受者)实现了 Value 接口( String 和 Set 方法),此时该类型就可以赋值给 Value 接口变量。
// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
// StringVar方法:将命令行参数的默认值value赋值给变量*p,并生成结构Flag并置于接受者中f.formal
func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
f.Var(newStringValue(value, p), name, usage) // newStringValue返回值是*stringValue类型,之所以能赋值给Value接口是因为newStringValue实现Value接口时定义的接受者为*stringValue
}flag文件夹中有`flag_test`包
flag 文件夹下有 flag_test 包,是因为该文件夹下包含了核心代码 flag.go 和测试代码 *_test.go 。这两部分代码并没有通过文件夹来区分。所以该 flag_test 包存在的意义是将测试代码与核心代码区分出来。而该包被引用时只会使用到核心代码。
// example_test.go
package flag_test作用域
关于作用域 Golang变量作用域 和 GO语言圣经中关于作用域 都有了详细的介绍,前者更通俗易懂些,后者更专业些。在 flag 包的 TestUsage 测试样例中,因为 func(){called=true} 是在函数 TestUsage 中定义函数,并且直接作为形参传递给 ResetForTesting 函数,所以该函数是和局部变量 called 是同级的,当然在该函数中给该变量赋值也是合理的。
// called变量的作用域
func TestUsage(t *testing.T) {
called := false
// 变量called的作用域
ResetForTesting(func() { called = true })
if CommandLine.Parse([]string{"-x"}) == nil {
t.Error("parse did not fail for unknown flag")
} else {
t.Error("hahahh")
}
if !called {
t.Error("did not call Usage for unknown flag")
}
}参考文献
- Go 夜读之 flag 包视频
- 实效 Go 编程之内存分配
- Go 语言设计与实现之 make 和 new
- 菜鸟教程之 Go 语言变量作用域
- Go 语言圣经中关于作用域
- Go 语言中值 receiver 和指针 receiver 的对比
- Go CodeReviewComments
- Golang 变量作用域
- Go 语言圣经中关于作用域
- Go 语言设计与实现之接口
Discuss 2018 05 08 Anlayze Underscore In Go (`content/discuss/2018-05-08-anlayze-underscore-in-go.md`)
title: 2018-05-08 Go 语言中下划线的用法分析总结 date: 2018-05-08T00:00:00+08:00
讨论时间:2018-05-08 19:25 ~ 2018-05-08 20:00
以下源码分析来源于 Go 夜读微信群的一次代码讨论,我们先来看看这一行代码吧。
...
func (littleEndian) Uint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
func (littleEndian) PutUint64(b []byte, v uint64) {
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56) // panic: runtime error: index out of range
}
...源码可点击 golang/encoding/binary/binary.go
这是 early bounds check to guarantee safety of writes below 注释的 commit。
直译:“早期检查以确保下面的写入安全”。
我们怎么理解这句话呢?
- _可以在编译期检查
- 怎么保证可以做到早期检查呢?
- 如果出现数组越界,会在编译期就不通过。
- 是不是单纯为了如果数组长度不够就提前报错返回,不进行后面的赋值操作了。
不确定究竟是为什么,所以大家还自己写代码来实测验证一下。
寻找答案
StackOverflow 有一个一摸一样的问题(他的问题写的非常完整,还给出了另外一种优化,并引发了他的更深刻的思考),以下是 Google 翻译,方便大家查看。
CodeA:
package main
import "fmt"
func main() {
b := []byte{0, 1, 2, 3, 4, 5, 6}
var v uint64 = 0x0807060504030201
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56) // panic: runtime error: index out of range
fmt.Println(b)
}b[7] 是会越界的,但是以上代码是可以通过编译的,只是在执行的时候会报错。
CodeB:
package main
import "fmt"
func main() {
b := []byte{0, 1, 2, 3, 4, 5, 6}
var v uint64 = 0x0807060504030201
b[7] = byte(v >> 56) // panic: runtime error: index out of range
b[6] = byte(v >> 48)
b[5] = byte(v >> 40)
b[4] = byte(v >> 32)
b[3] = byte(v >> 24)
b[2] = byte(v >> 16)
b[1] = byte(v >> 8)
b[0] = byte(v)
fmt.Println(b)
}在一开始 b[7] = byte(v >> 56) 写入数据时就 panic 了。
CodeC:
package main
import "fmt"
func main() {
b := []byte{0, 1, 2, 3, 4, 5, 6}
var v uint64 = 0x0807060504030201
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56)
fmt.Println(b)
}通过 _ = b[7] 做了早期检查以确保下面的写入安全。
这个问题的作者抛出来两个问题:
- Q1: 在 Golang 中是否有必要进行早期检查以保证书写的安全性?
- Q2: 为了保证书写的安全性进行早期检查的话,哪一个样本代码更简洁和性能优化(速度)好些呢?样本代码A,B,C或...?
- 作者的回答: 我认为是 B: 因为它简洁并做了早期检查,不是吗?
回答:
问题1:在 Golang 中是否有必要进行早期检查以保证书写的安全性?
A1:这里的答案是“是和否”。一般来说,“否”,你通常不必在Go中插入边界检查,因为编译器会为你插入它们(这就是为什么当你尝试访问片段长度之外的内存位置时,你的示例会 panic)。但是,如果你正在执行多个写入操作(“是”),则需要插入像提供的示例一样的早期边界检查,以确保你不会只有一些写入成功,从而使你处于不良状态(或重构,如你在示例B中所做的那样,以便首次写入最大阵列,确保在任何写入操作成功之前发生恐慌)。
然而,这不是一个“Go 问题”,因为它是一个通用的错误类。在任何语言中如果你不进行边界检查(或者如果它是一种强制执行像 Go 一样的边界的语言的最高索引),写入操作就不安全。这也很大程度上取决于解决方案;在你发布的标准库的示例中,用户进行边界检查是有必要的。但是,在你发布的第二个示例中,用户边界检查不是必需的,因为代码可以像 B 一样写,其中编译器会在第一行插入边界检查。
问题2:为了保证书写的安全性进行早期检查的话,哪一个样本代码更简洁和性能优化(速度)好些呢?样本代码A,B,C或...?
A2:我认为是 B: 因为它简洁并做了早期检查,不是吗?
你是对的。在 B 中,编译器会在第一次写入时插入边界检查,保护其余的写入。因为你正在使用常量(7,6,... 0)对切片进行索引,编译器可以将边界检查从其余的写入中删除,因为它可以保证它们是安全的。另外一个人的回答:
关于“写入安全性”的评论在这里有误导性。在开始时放置最高边界检查只是一个优化。如果你忽略它,行为将不会改变(或变成“不安全”),但是你可能会遭受多重边界检查而不是仅仅一次的性能损失,因为每个后续较高索引所需的最小限度增量。
如果评论中提到“保证写入安全”,这只是意味着它将保证编译器后续的写入操作是安全的,无需插入更多的边界检查。把它写出来不会使写入不安全,只会让编译器插入更多的边界检查。在任何情况下,编译器都不会产生不安全的内存访问。
在代码中插入这个假的早期边界检查是一个好主意,而不是不使用它或者重写代码来合法使用最高索引(如代码B中的代码),这是值得商榷的。只要它清楚为什么它在那里(例如,一个明智的和没有误导性的评论)我会说如果你想使用它,并找到它的好处。一般情况下,通过手动优化,未来的编译器优化可能会使其成为冗余或以其他方式改变其有效性。Go 语言中下划线的用法总结
用在 import
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
_ "net/http/pprof"
)
...pprof 和 MySQL 是常见用法。
它会引入包,会先调用包中的 init() 函数,这种使用方式仅让导入的包做初始化,而不使用包中其他函数。
往往这些
init()函数里面注册了自己包里面的引擎,让外部可以方便的使用,比方说很多实现 database/sql 的引擎,在init()函数里面都是调用了 sql.Register(name string, driver driver.Driver) 注册自己,然后外部就可以使用了。
程序的初始化和执行都起始于 main 包,如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被 导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它 包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下面是 init 的整个详细执行过程:
用在返回值
for _,v := range Slice{} // 表示丢弃索引值。
但是有些时候返回值不能丢弃,丢弃了会导致 memory leak,举个栗子:当你使用 http 请求第三方接口时,如果丢弃 response,那么 response 的 body 系统不会帮你 close,所以会导致很多的 time_wait,然后内存会缓慢上升。
_, err := func() // 单函数有多个返回值,用来获取某个特定的值,其他值不获取。
用在变量(接口实现检查)
首先我们来看 gin 框架的源代码 ResponseWriter
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
// Returns the HTTP response status code of the current request.
Status() int
// Returns the number of bytes already written into the response http body.
// See Written()
Size() int
// Writes the string into the response body.
WriteString(string) (int, error)
// Returns true if the response body was already written.
Written() bool
// Forces to write the http header (status code + headers).
WriteHeaderNow()
}
type responseWriter struct {
http.ResponseWriter
size int
status int
}
var _ ResponseWriter = &responseWriter{}其中 ResponseWriter 为 interface,用来判断 responseWriter 结构体是否实现了 ResponseWriter,用作类型断言,如果 responseWriter 没有实现接口 ResponseWriter,则编译错误。
更多源码,点击前往:gin/response_writer.go
延伸阅读
- https://stackoverflow.com/questions/38548911/is-it-necessary-to-early-bounds-check-to-guarantee-safety-of-writes-in-golang
- Bounds Checking Elimination
- cmd/compile: unnecessary bounds checks are not removed #14808
Discuss 2018 05 09 Wechat Discuss (`content/discuss/2018-05-09-wechat-discuss.md`)
title: 2018-05-09 微信群讨论 date: 2018-05-09T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-09
1. debug
调试:dlv
(待补充)
2. not reached
go/src/runtime/panic.go
(待补充)
3. Go 开发工具
Vim:(待补充)
Emacs
VSCode
JetBrains: IntelliJ,Goland
- Goland用学生邮箱可以免费
- 服务器认证(这个就不贴了,大家自行Google)
- 其他更多
LiteIDE: LiteIDE的跟踪代码很不错,可以同时在一个窗口打开多个项目的目录.
4. 问题
var x string
func init() {
x, err := getValue()
}有一个全局变量 x,在 init() 函数里面赋值,然后获取 x 的值发现全局变量未赋值,这种情况有什么优雅的解决方法吗?
var x string
func init() {
var err error
x, err = getValue()
}这个:=为什么不能对全局变量起作用呢?因为它成为局部变量了,屏蔽了全局变量作用域。
var x string
func init()(err error) {
x, err = getValue()
}以上代码是 错误 的,Go 语言中 main() 和 init() 函数都不能有返回值,否则编译会报错:
func init must have no arguments and no return values
func main must have no arguments and no return values参考资料
Discuss 2018 05 10 Which Vendor Tool (`content/discuss/2018-05-10-which-vendor-tool.md`)
title: 2018-05-10 用什么工具解决项目依赖 date: 2018-05-10T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-10 13:30
1. 你们是用什么工具解决项目依赖的呢?
- dep
- govendor
govendor 最简洁,glide最方便,不过一般团队用啥就用啥。
2. 平时你们的工作目录结果是怎样的?
一个大的 GOPATH 下,放所有项目【比较多是这种方式】
一个项目一个 GOPATH
有一个默认 GOPATH,不同项目有不同的 GOPATH
VisualStudio Code 的设置:
"go.gopath": "${workspaceRoot}:/Users/username/gopath"
公共第三方包是公用的,每个有自己的 vendor,编译打包什么的都是各自的 makefile(很多Github上的大项目,比如docker,都是自己写 Makefile ,整理项目依赖,没有一个统一的形式)
Rust 的项目结构比 Go 好,Rust 的 cargo 还不错。
- 可以给 GOPATH 写一个脚本
gop() {
if [ "$1" = "" ]; then
elif [ "$1" = "d" ]; then
export GOPATH=`echo $DEF_GOPATH`
elif [ "$1" = "a" ]; then
export GOPATH=`echo $DEF_GOPATH`:`pwd`
elif [ "$1" = "f" ]; then
export GOPATH=`pwd`
fi
echo "current GOPATH = "$GOPATH
}参考资料
Discuss 2018 05 13 Declaring Variables On If Else (`content/discuss/2018-05-13-declaring-variables-on-if-else.md`)
title: 2018-05-13 关于变量在 if-else 条件表达式里的作用域范围 date: 2018-05-13T00:00:00+08:00
一. 如何发现这个问题?
我是在浏览 Twitter 的时候, 发现博主 David Crawshaw 分享了一段代码,
推文地址:
https://twitter.com/davidcrawshaw/status/994614426064928770
原博文代码如下, 点击在 play 里运行:
package main
import "fmt"
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
fmt.Println(a, b, c) // 结果: 1 2 3
}
}所以, 就扩展一下如下这种方式:
package main
import "fmt"
func main() {
if a := 1; false {
aa := 11
} else if b := 2; false {
bb := 22
} else if c := 3; false {
cc := 33
} else {
fmt.Println(a, b, c)
fmt.Println(aa, bb, cc) // 结果: undefined aa bb cc
}
}再尝试如下这种方式:
package main
import "fmt"
func main() {
if x := 10; x < 9 {
fmt.Println(x)
} else {
fmt.Println("not x")
}
fmt.Println(x) // 结果: other if-else.go:11:14: undefined: x
}脑洞再大点:
package main
import "fmt"
func main() {
if a := 1; false {
fmt.Println(a, b, c) // undefined: b, c
} else if b := 2; false {
fmt.Println(a, b, c) // undefined: c
} else if c := 3; false {
fmt.Println(a, b, c)
} else {
fmt.Println(a, b, c)
}
}合理的猜测:
package main
import "fmt"
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
if a := 11; true {
fmt.Println(a, b, c) // 11 2 3
}
fmt.Println(a, b, c) // 1 2 3
}
}所以可以得出一个初步的结论:
只有 if/else-if 条件表达式里的变量声明作用域才会向下到达最后 else 内部, 显而易见的不能向上作用, 作用域范围仅限于本级, 不影响正常的变量作用范围以及屏蔽作用.
二. 这么做有什么好处和坏处?
A. 好处:
- 如下面
Chris Hines评论列出的代码 , 在处理 sql 任务的时候, 对返回值和错误做处理, 逻辑上看, 比较流畅.
if result, err := db.Exec(updateSql, ...); err != nil {
return err
} else if count, err := result.RowsAffected(); err != nil {
return err
} else if count != 1 {
return ErrNotUpdated
}
- 另一个评论的代码,
emmmmm, 怎么说呢? 这么看代码确实比较简洁优美一些, 不过对于不了解这个特性的人来说, 可能有坑, 增加阅读难度.
if got, err := f(); err != nil {
// 巴拉巴拉巴拉
} else if want := ...; got != want {
t.Errorf(..., got, want)
}好在我们可以加注释. 改成如下, 在可读性上会好点:
// NOTE: golang 特性, got 的作用域贯穿整个 if-else...
if got, err := f(); err != nil {
// 巴拉巴拉巴拉
} else if want := ...; got != want {
t.Errorf(..., got, want)
}
- 语言的设计就是如此:
参考设计文档: https://golang.org/ref/spec#If_statements, 官方并没有明确的说这么做的好处. 不过确实给书写代码带来了方便.
B. 坏处:
- emmmmm, 就是对于不知道这个特性的人来说比较奇怪. 不是很理解, 不符合编程的核心原则: 代码首先是给人看的, 顺便给机器运行.
Discuss 2018 05 16 The Way To Go (`content/discuss/2018-05-16-the-way-to-go.md`)
title: 2018-05-16 Go 学习之路 date: 2018-05-16T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-16
推荐新手先看一看Go 学习之路
1. 有没有适合小白看的项目?(没有底层语言基础)
- 建议先系统过一遍基础语法;
- 看了 Go 基础之后,可以把数据结构,排序:树、表等都撸一遍;
- 自己写点工具、爬虫之类的进步比较快;
- 推荐『Go 语言实战』,『Go 语言系列』,适合从入门到深入
- 编程书籍的整理和收集
- 雨痕大神些的『Go 语言学习笔记』也不错,包含语法介绍和源码剖析(可能不适合初学者哦,比较多的底层知识)
推荐项目
- beego
- BoltDB(不维护了,但是代码不多,注释比较详细,作为学习容易入门)
- urfave/cli
- grpc-gateway
- grpc-go
Discuss 2018 05 18 Bitset And Import Cycle Not Allowed (`content/discuss/2018-05-18-bitset-and-import-cycle-not-allowed.md`)
title: 2018-05-18 包循环依赖如何解决 source: 『Go 夜读』微信群 date: 2018-05-18T00:00:00+08:00
1. 包循环依赖如何解决?
方法:
用 govendor list -v 可以查看一个包被哪些包依赖。
详解:
使用 govendor list -v 可以查看一个包被哪些包依赖:
那么反过来,你可能想知道一个包依赖了哪些包?这个是 go 工具链里面提供的方法,直接使用 go list,比如:go list -f '{{ .Imports }}' github.com/talkgo/night
2. bitset
strings 包之 bitset,一个实现:https://github.com/henrylee2cn/goutil/blob/master/bitset/bitset.go
对于以上代码的一些讨论:
Q:1. 没有看到 unset,或者 clear 清除标志,2. 其次位的设置可以用 atomic 的 or 或操作,无锁编程效率更高。
A:可以加 clear,如果用原子锁就不能用 []byte 类型实现了。
Q:count 实现的太粗暴了,直接就一个一个的遍历,无法利用到 simd 和底层 cpu 指令。
A:count 字段可以用缓存字段,也就是可以用 map,也可以用 `math/bits` 这个库来实现。
Q:[]byte 为什么没办法使用原子锁,地址是有办法获得的。
A:那你知道锁的底层也是原子锁实现的吧?既然还是整个切片上锁,换成原子锁也一样的。其实用库是最后的,这种实现不是最好的,往往 Go 的库能够实现 simd,在 go 1.x,编译期开始了对 simd 的支持。刚好算 byte 中置 1 的数目这个是可以很好的被 simd 的,这个叫 POPCNT ,机器指令,真正的 o(1)。
simd 是什么?
单指令流多数据流(英语:Single Instruction Multiple Data,缩写:SIMD)是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。
在微处理器中,单指令流多数据流技术则是一个控制器控制多个平行的处理微元,例如Intel的MMX或SSE,以及AMD的3D Now!指令集。
图形处理器(GPU)拥有强大的并行处理能力和可程式流水线,面对单指令流多数据流时,运算能力远超传统CPU。OpenCL和CUDA分別是目前最广泛使用的开源和专利通用图形处理器(GPGPU)预算语言。--摘自simd-维基百科。
Go 官方库的实现:
// math/bits/bits.go
// 位计数中的分治法
// OnesCount64 returns the number of one bits ("population count") in x.
func OnesCount64(x uint64) int {
// Implementation: Parallel summing of adjacent bits.
// See "Hacker's Delight", Chap. 5: Counting Bits.
// The following pattern shows the general approach:
//
// 实现:相邻位的并行求和。
// 见“黑客的喜悦”(见:算法心得:高效算法的奥秘(中文第2版)),章节.5:计数位。
// 以下模式显示了一般方法:
// x = x>>1&(m0&m) + x&(m0&m)
// x = x>>2&(m1&m) + x&(m1&m)
// x = x>>4&(m2&m) + x&(m2&m)
// x = x>>8&(m3&m) + x&(m3&m)
// x = x>>16&(m4&m) + x&(m4&m)
// x = x>>32&(m5&m) + x&(m5&m)
// return int(x)
//
// Masking (& operations) can be left away when there's no
// danger that a field's sum will carry over into the next
// field: Since the result cannot be > 64, 8 bits is enough
// and we can ignore the masks for the shifts by 8 and up.
// Per "Hacker's Delight", the first line can be simplified
// more, but it saves at best one instruction, so we leave
// it alone for clarity.
// 如果不存在字段总和将传送到下一个字段的危险,则可以将掩码(&操作)留空:由于结果不能大于64,所以8位就足够了,我们可以忽略8位以上的移位掩码。 根据“Hacker's Delight”,第一行可以简化得更多,但它最多可以节省一条指令,所以为了清晰起见,我们只保留一条。
const m = 1<<64 - 1
x = x>>1&(m0&m) + x&(m0&m)
x = x>>2&(m1&m) + x&(m1&m)
x = (x>>4 + x) & (m2 & m)
x += x >> 8
x += x >> 16
x += x >> 32
return int(x) & (1<<7 - 1)
}另外一个文档解释的非常清楚:Efficient_implementation
强烈推荐阅读(只能用于学习查阅,请勿分享传播,如有侵权,请联系我):
其他
Wikiwand是一款能够改变维基百科条目界面的软件,2013年由利奥尔·格罗斯曼和依兰·列文创建,2014年8月正式上线。软件界面包含工具栏菜单、导航栏、其他语言版本的个性化链接、新版面和链接条目的预览。内容列表将在左侧不断显示。--摘自wikiwand 维基百科
参考资料
- https://golang.org/pkg/math/bits/
- https://golang.org/src/math/bits/bits.go
- https://github.com/henrylee2cn/goutil#bitset
- simd - 单指令流多数据流
- https://www.wikiwand.com/en/Hamming_weight#/Efficient_implementation
- https://zh.wikipedia.org/wiki/Wikiwand
Discuss 2018 05 21 Using Goroutines On Loop Iterator Variables (`content/discuss/2018-05-21-using-goroutines-on-loop-iterator-variables.md`)
title: 2018-05-21 微信讨论 date: 2018-05-21T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-21
1. 如下代码何解?
a := []int{1,2,3}
for _, i:= range a {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
// Output:
// 3
// 3
// 3大家可以看参考资料 1, 2,上面有完整的解答。
延伸出来的知识点:惰性求值和闭包。
解答:
根据问题的描述推断出是闭包的特性,通过Google检索关键字 glang faq loop closure 可找到官方的faq,里面有详细的解释,并提醒了用户可以通过 go vet命令来检查程序中类似的问题。
最后官方的faq还给出了针对这个问题的两种解决方法。其中第一种方法很好理解,就是把变量v的值通过参数的形式传递给goroutine,因为go中func的参数传递都是值传递,所以就在goroutine启动时获得了当前v变量的值。
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}第二种方法是更巧妙地通过一个赋值语句来解决的。
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}如果没有明白其中的原理,只要自己把赋值前后变量v的内存地址打印出来就明白了,在赋值后新的变量v实际上是在内存中开辟了一个新的空间并保存了当前变量v的值,两个变量并不是指向相同的内存地址。
for _, v := range values {
fmt.Printf("Before assignment:%p\n", &v)
v := v
fmt.Printf("After assignment:%p\n", &v)
go func() {
fmt.Println(v)
done <- true
}()
}
// Output:
// Before assignment:0xc04206c080
// After assignment:0xc04206c090
// Before assignment:0xc04206c080
// After assignment:0xc04206c0a0
// Before assignment:0xc04206c080
// After assignment:0xc04206c0b02. []int 转 []int32 有没有什么好办法?
?
3. 大家的公司一般都用什么用于监控?
- zabbix
- grafana+influxdb
- openfalcon
参考资料
Discuss 2018 05 22 Go String To Byte Slice (`content/discuss/2018-05-22-go-string-to-byte-slice.md`)
title: 2018-05-22 Go string to byte slice date: 2018-05-22T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-22
背景
去面试的时候遇到一道和 string 相关的题目,记录一下用到的知识点。题目如下:
s:="123"
ps:=&s
b:=[]byte(s)
pb:=&b
s+="4"
*ps+="5"
(*pb)[1] = 0
(*pb)[2] = 4
fmt.Printf("%+v\n",*ps)
fmt.Printf("%+v\n",*pb)问以上代码的输出是什么。
分析
很容易可以看出 s 和 ps 代表同一个 string,b 和 pb 代表同一个 byte 的切片。关键在于
b:=[]byte(s)根据 The Go Programming Language 的解释:
A string contains an array of bytes that, once created, is immutable. By contrast, the elements of a byte slice can be freely modified.
Strings can be converted to byte slices and back again:s := “abc”
b := []byte(s)
s2 := string(b)Conceptually, the []byte(s) conversion allocates a new byte array holding a copy of the bytes of s, and yields a slice that references the entirety of that array. An optimizing compiler may be able to avoid the allocation and copying in some cases, but in general copying is required to ensure that the bytes of s remain unchanged even if those of b are subsequently modified. The conversion from byte slice back to string with string(b) also makes a copy, to ensure immutability of the resulting string s2.
因为 string 是不可变的,所以不管是从 string 转到 []byte 还是从 []byte 转换到 string 都会发生一次复制。因此 p 和 b 可以看作两个内容相同的两个对象,对 p,b各自的修改不会影响对方。
先看 ps,经过两次拼接后就是 "12345"。
再看 pb,因为 s 中的内容都是 ASCII 字符,在 b 中只需要用一个 byte 就可以表示一个字符,所以 b 的实际内容是[49,50,51],分别对应1,2,3的 ASCII 编码。经过就修改后就变成了[49,0,4]。
答案
经过上面的分析,最后输出的答案是"12345"和[49,0,4]
延伸
题目中对字符串的拼接也是常用的场景。因为 string 是不可变的,所以在拼接字符串时实际上也是将源字符串复制了一次,所以在 string 比较大时会消耗不少的内存和时间。关于字符串拼接的各种方法这里不详细展开了,有兴趣的可以参考以下几个链接:
https://gocn.io/question/265
https://gocn.io/article/704
Reference
https://blog.golang.org/strings
https://sheepbao.github.io/post/golang_byte_slice_and_string/
http://nanxiao.me/golang-string-byte-slice-conversion/
Discuss 2018 05 23 Wechat Discuss (`content/discuss/2018-05-23-wechat-discuss.md`)
title: 2018-05-23 Get passes lock by value date: 2018-05-23T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-23
1. Get passes lock by value
type ExecuterList struct {
sync.Map
length int
}
func (e ExecuterList) Get(key string) IExecuter {
value, ok := e.Load(key)
if !ok {
return nil
}
if value == nil {
return nil
}
res, _ := value.(IExecuter)
return res
}使用 go tool vet ,出现“Get passes lock by value: ExecuterList contains sync.Map contains sync.Mutex”, 解决方案有两种:。
1,sync.Map用指针
type X struct {
*sync.Map
}2, 也可以用 (e *ExecutorList) ,避免锁的复制。
为什么会失效呢?
因为你每次操作都复制了一遍整个struct,当然也复制了Map里面的Mutex,多线程同时读写时Map里面的锁相当于失效了。理解这个你需要知道的知识点有两个,一是go的参数都是值传递,二是只有用同一把锁才能对某个资源边界进行锁与解锁的操作。
2. RPC 微服务框架
rpc 可以封装程序间网络通信层,服务间调用只需要关注目标服务是什么,不用关心我到底用什么协议和数据格式了。
Discuss 2018 05 28 Pprof In Go (`content/discuss/2018-05-28-pprof-in-go.md`)
title: 2018-05-28 生产环境如何调试服务的内存泄露 date: 2018-05-28T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-05-28
1. 生产环境如何调试服务的内存泄露?
- pprof
- 火焰图
待补充...
如果用 gin 的话, 就一句代码.....https://github.com/DeanThompson/ginpprof
其他方法:目测法、删除法(删除怀疑的代码,然后压测)。
2. pprof 的数据如何实时采集到 influxdb ,有什么解决方案?
pprof 不能保存到 influxdb,想要保存的话,得使用 expvar 把 Golang 应用的内部数据传过去。
pprof 是一个运行时间段的数据,然后后续分析使用,线上应该使用 APM 方案。
pprof 算是监控的一种,promethues 中自带的 exporter 就有监控 go runtime 的数据,比如 goroutine 数量,栈等。
3. 返回的 int和interface的区别,如下图:
dlv debug 一下,运行顺序是一样的,是不是 golang 的 bug 呢?
看汇编,不是完全看的懂,如果是interface的时候,会申请一个type为int空interface的结构,然后想把m的值复制给data区,但是结果就是没有成功。
参考资料
- Go 自带 pprof
- 容器环境下 go 服务性能诊断方案设计与实现
- Golang 逃逸分析
- spec: order of evaluation of variables in return statement is not determined
- spec: Order_of_evaluation
Discuss 2018 06 07 Dial Timeout In Go (`content/discuss/2018-06-07-dial-timeout-in-go.md`)
title: 2018-06-07 Dial 超时 date: 2018-06-07T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-06-07
问题1
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) {
func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {这两个方法返回的对象类型不一样,想得到TCPConn对象,同时可以设置连接超时,应该怎么写?有谁可以帮帮忙吗?
回答:
Use net.Dialer with either the Timeout or Deadline fields set.
d := net.Dialer{Timeout: timeout}
conn, err := d.Dial("tcp", addr)
if err != nil {
// handle error
}A variation is to call Dialer.DialContext with a deadline or timeout applied to the context.
Type assert to *net.TCPConn if you specifically need that type instead of a net.Conn:
tcpConn, ok := conn.(*net.TCPConn)参考资料
Discuss 2018 07 02 C1000k On Linux (`content/discuss/2018-07-02-c1000k-on-linux.md`)
title: 2018-07-02 在Linux操作系统环境,单机如何支撑100万并发的服务 date: 2018-07-02T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-02
问题1. 在Linux操作系统环境,单机如何支撑100万并发的服务
- 问题来源:
HundredLee 在微信群里分享了一个掘金网的文章 GO千万级消息推送服务,然后另一个大佬 bye 说 goim 是哔哩哔哩在用的一个框架,可以支撑百万级并发连接,于是在“单机百万并发”这个问题上展开了热烈的讨论。
问题:我以前对并发的理解几乎都是错的,误以为每有一个客户端连接到服务器上就会多占用服务器的一个新TCP端口,但是看了文章和其他的QQ群的讨论,我终于搞明白了并发的概念。那么我的疑惑是单机如何才能支撑起100万的并发量呢?
答案
Linux服务器的并发连接数需要优化内核参数,在使用比较新的内核版本时,会有更好的优化效果。并发连接数主要和操作系统允许打开的文件描述符数量有关,需要设置 fs.file-max 和 fs.nr_open 参数,fs.file-max 参数设置的是整个操作系统允许创建的文件描述符数量的最大值,fs.nr_open 参数设置的是每个进程各自最多可以打开的文件描述符数量,这两个值限制 pam_limits.so 模块定义 的可打开文件描述符数量的参数上限。pam_limits.so 模块在定义了可打开的文件描述符数量的同时也就规定了 socket
的最大数量。所以服务能否支撑百万连接数首先取决于内核参数,然后取决于 pam_limits.so 模块的限制,pam_limits.so 模块决定了每个用户和组可用的资源。
另外还要对每一个TCP套接字连接占用的内存做分析,大概占4KByte;维护百万连接需要占用的内存就是 1000000 * 4KByte,大约是4GByte,还没有计算进去发送消息的开销,保守估算需要的内存是16GByte,如果要推送消息给这100万客户端,很显然非常吃力,反过来如果是100万的客户端排队发请求,就可以轮询处理队列里的请求了。
参考
- 大战C100K之4-Linux内核调优篇
- linux内核调优支持500K并发
- 通过ulimit和PAM来限制资源
- 关于 TCP 并发连接的几个思考题与试验
- Linux下Http高并发参数优化之TCP参数
- Linux TCP/IP 协议栈调优
- 单机千万并发连接实战
- 构建C1000K的服务器(1) – 基础
- 构建C1000K的服务器(2) – 实现百万连接的comet服务器
- Linux 中每个 TCP 连接最少占用多少内存?
- 关于pam_limits资源限制的回答
- Debian官方文档对于Limits的详细解释
Discuss 2018 07 04 Package Names (`content/discuss/2018-07-04-package-names.md`)
title: 2018-07-04 文件夹命名 date: 2018-07-04T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-04
问题1. 话说大家 go 文件夹命名 是什么规范。下划线 横线 驼峰?有标准么?
有标准 https://blog.golang.org/package-names
问题2. 包的命名多个单词的情况呢?
包名和文件名用小写,使用短命名,尽量和标准库不要冲突。
文件名或者文件夹不要用大写,切记!!!
系统有些区别大小写,有些不区分,比如 Mac 不区分大小写,而 linux 区分。
制作规范的时候参考的是 effective go。
参考
Discuss 2018 07 09 Make New In Go (`content/discuss/2018-07-09-make-new-in-go.md`)
title: 2018-07-09 怎么建立一个一一映射? date: 2018-07-09T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-09
问题1. golang 怎么建立一个一一映射,现在的映射 map 是可以多对一的是吧?
建立两个 map 互查。
segment tree?
问题2. golang 中 new 和 make 关键字的区别和使用场景?
首先,我们看一下 Go 标准包的 builtin 源码吧(go/src/builtin/builtin.go)
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Typenew 它接受一个参数,这个参数是一个类型,不是一个值,分配好内存后,返回一个指向该类型内存地址的指针,同时请注意它同时把分配的内存置为零,也就是类型的零值。
new 不常用:new 在一些需要实例化接口的地方用的比较多,但是可以用 &A{} 替代。
但是 new 和 &A{} 也是有差别的,主要差别在于 &A{} 显示执行堆分配。
make 也是用于内存分配的,但是和new不同,它只用于channel,map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
注意:这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个跟new是不一样的。
举例说明:
package main
import "fmt"
func main() {
p := new([]int) //p == nil; with len and cap 0
fmt.Println(p)
v := make([]int, 10, 50) // v is initialed with len 10, cap 50
fmt.Println(v)
/*********Output****************
&[]
[0 0 0 0 0 0 0 0 0 0]
*********************************/
(*p)[0] = 18 // panic: runtime error: index out of range
// because p is a nil pointer, with len and cap 0
v[1] = 18 // ok
}
// 作者:iCaptain
// 链接:https://www.jianshu.com/p/c173dab0e71c
// 來源:简书
// 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。go 的逃逸分析决定了是分配到堆上还是栈上。
参考
- Effective Go#allocation_new
- Effective Go#allocation_make
- Go 语言机制之逃逸分析(Language Mechanics On Escape Analysis)
- Go 的变量到底在堆还是栈中分配
- Golang 变量逃逸分析小探
Discuss 2018 07 11 Using 64bit Atomic In 32bit System (`content/discuss/2018-07-11-using_64bit_atomic_in_32bit_system.md`)
title: 2018-07-11 在32位系统中使用64位原子操作的坑 date: 2018-07-11T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-11
在32位系统中使用64位原子操作的坑
先来个例子
type Y struct {
a bool
v uint64
}
func TestAtomicY(t *testing.T) {
var y Y
atomic.AddUint64(&y.v, 1) // panic in 32bit system
}在上面的例子中,如果在64位系统中运行是没问题的的,但是在32位系统中会panic。
为何会 Panic ?
这个简单回答可以查看,go官方的文档atomic-pkt-note的内容:
Bugs
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX.
On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.
On both ARM and x86-32, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.意思就是你如果要在32位系统中用64位的原子操作,必须要自己保证64位对齐,也就是8字节对齐。
如果要看汇编怎么判断的可以查看atomic·Xadd64
如果需要在一个struct中维护多个64位字段,且都需要原子操作,怎么办?
最简单的办法就是将所有64位字段放在struct的头部,这样就可以保证8字节对齐,如果你把这个struct嵌入在别的结构体,也要记得嵌入到头部。
一些例子
package aotmic_test
import (
"log"
"sync/atomic"
"testing"
"unsafe"
)
type X struct {
v uint64
x uint64
a bool
z uint64
y uint32
}
func TestAtomic(t *testing.T) {
var x X
log.Printf("x.a=%p, offset=%d, alig=%d", &x.a, unsafe.Offsetof(x.a), unsafe.Alignof(x.a))
log.Printf("x.v=%p, offset=%d, alig=%d", &x.v, unsafe.Offsetof(x.v), unsafe.Alignof(x.v))
log.Printf("x.x=%p, offset=%d, alig=%d", &x.x, unsafe.Offsetof(x.x), unsafe.Alignof(x.x))
log.Printf("x.y=%p, offset=%d, alig=%d", &x.y, unsafe.Offsetof(x.y), unsafe.Alignof(x.y))
log.Printf("x.z=%p, offset=%d, alig=%d", &x.z, unsafe.Offsetof(x.z), unsafe.Alignof(x.z))
log.Printf("x.v=%p", &x.v)
atomic.AddUint64(&x.z, 1) // panic
}
type Y struct {
a bool
X
}
func TestAtomicY(t *testing.T) {
var y Y
x := y.X
atomic.AddUint64(&x.v, 1)
atomic.AddUint64(&y.X.v, 1) // panic
}
type Y2 struct {
X
a bool
}
func TestAtomicY2(t *testing.T) {
y := &Y2{}
atomic.AddUint64(&y.X.v, 1)
}
type Temp struct {
A byte
B [2]byte
C int64
}
func TestAtomicTemp(t *testing.T) {
var x Temp
log.Printf("sizof=%d", unsafe.Sizeof(x))
log.Printf("x.A=%p, offset=%d, alig=%d", &x.A, unsafe.Offsetof(x.A), unsafe.Alignof(x.A))
log.Printf("x.B=%p, offset=%d, alig=%d", &x.B, unsafe.Offsetof(x.B), unsafe.Alignof(x.B))
log.Printf("x.C=%p, offset=%d, alig=%d", &x.C, unsafe.Offsetof(x.C), unsafe.Alignof(x.C))
}参考链接
https://go101.org/article/memory-layout.html
https://github.com/golang/go/issues/5278
Discuss 2018 07 14 Version Gopath Go Command (`content/discuss/2018-07-14-version-gopath-go-command.md`)
title: 2018-07-14 包版本管理 date: 2018-07-14T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-14
问题1:包版本管理?
govendor,vgo?
vgo 是 Go Module 的前身。
问题2:为什么要搞一个 GOPATH ?
GOPATH 是 Go 最初设计的产物,在 Go 语言快速发展的今天,人们日益发现 GOPATH 似乎不那么重要了,尤其是在引入 vendor 以及诸多包管理工具后。并且 GOPATH 的设置还会让 Go 语言新手感到些许困惑,提高了入门的门槛。Go core team 也一直在寻求 “去 GOPATH” 的方案,当然这一过程是循序渐进的。Go 1.8 版本中,如果开发者没有显式设置 GOPATH,Go 会赋予 GOPATH 一个默认值(在 linux 上为 $HOME/go )。虽说不用再设置 GOPATH,但 GOPATH 还是事实存在的,它在 go toolchain 中依旧发挥着至关重要的作用。
问题3:go get 命令内部应该也是用 git clone 命令吧?
https://github.com/golang/go/wiki/GoGetTools
https://github.com/hyper0x/go_command_tutorial/blob/master/0.3.md
参考链接
- https://github.com/golang/go/wiki/PackageManagementTools
- 初窥 Go Module
- https://git-scm.com/docs/revisions
- https://dave.cheney.net/2018/07/14/taking-go-modules-for-a-spin
Discuss 2018 07 31 Println Println And Context (`content/discuss/2018-07-31-println-Println-and_context.md`)
title: 2018-07-31 println 与 fmt.Println 有何猫腻 date: 2018-07-31T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-07-31
问题1:println与fmt.Println有何猫腻?
下面2个输出语句代码,哪一行先输出
package main
import "fmt"
func main() {
println("11111")
fmt.Println("22222")
}本人的是debian linux系统下,go version为1.9.1,goland IDE,反复执行几次之后发现始终"反着"输出
22222
11111按照一般人的思维惯性(或者说下意识),应该先输出11111,再输出22222,然后向Go夜读微信群发起提问,结果群友测试的结果就是顺序输出,他的测试环境是macbook, go version1.10.3
package main
import "fmt"
func main() {
println("aaaaa")
fmt.Println("bbbbb")
}输入
aaaaa
bbbbb难道是因为内容不同就不一样?难道是因为和系统有关?难道是和go版本有关?
我们看下println源代码,如下:
// 大概意思是:println内置函数将其参数格式化为特定实现方式,然后将结果写入标准错误流
// 而Println函数对于我们开发程序的时候调试输出很有用,且不能保证以后会留在语言中
// The println built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Spaces are always added between arguments and a newline is appended.
// Println is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func println(args ...Type)我们再看看Println源代码,如下:
// 大概意思是:Println函数使用默认格式化并将结果写入标准输出流
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}看了2个函数的实现之后,总结如下:
- println是输出到stderr
- fmt.Println是输出到stdout
fmt.Println writes go standard output(stdout) and println writes to standard error (stderr),two different,unsynchronized files
结论:linux中,标准输入,标准输出,标准错误分别对应三个管道,编号分别是0,1,2,不同的管道输出是并发,不存在不同的管道之间相互等待,也就不存在先后顺序,上面两个函数是输出到不同的输出管道,就有可能导致顺序不一致,一个终端总是会存在标准输出和标准错误输出。所以最开始的那个问题是随机输出,没有固定的顺序!,写个大点的循环就能容易发现是随机,不存在系统不一致,或者go版本不一致导致顺序可能不同。
问题2:context.Context普通用,是不是主要通过with value?
问题: context.Context普通用,是不是主要通过with value,可以传递部分参数而不用传递全部参数,然后调用的函数可以把对应的参数取出来?对这个问题,各位有什么看法?
答:参考链接:http://www.flysnow.org/2017/05/12/go-in-action-go-context.html
Discuss 2018 08 02 Apns Push Notification (`content/discuss/2018-08-02-apns-push-notification.md`)
title: 2018-08-02 Push Notification (apns) date: 2018-08-02T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-02
问题: 如何批量发送apns消息推送
Install apns2
go get -u github.com/sideshow/apns2先看下如何发送一条的apns.
func push(){
cert, err := certificate.FromP12File("../cert.p12", "")
if err != nil {
log.Fatal("Cert Error:", err)
}
notification := &apns2.Notification{}
notification.DeviceToken = "11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"
notification.Topic = "com.sideshow.Apns2"
notification.Payload = []byte(`{"aps":{"alert":"Hello!"}}`) // See Payload section below
client := apns2.NewClient(cert).Production()
res, err := client.Push(notification)
if err != nil {
log.Fatal("Error:", err)
}
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
}一开始会想到,当需要推送大量消息的时候,会想到每发一条推送的时候,开一个协程(当然也要限制协程的数量),像这样:
go push()但是一个问题,apns我记得是最多只能挂载15个tls.也就是最多同时发送15条推送消息,远远不能满足像IM需要大量同时推送的需求.
tls
而tls是14年的时候,苹果官方发布更换了所有的通讯协议:
"鉴于SSL 3.0最新发现的漏洞,为了保护用户,APNS决定在下周三也就是10月29号起开始停止对SSL 3.0的支持。所有仅支持SSL 3.0的推送服务需要更换为TLS以确保推送服务能够正常运行,同时支持了SSL 3.0和TLS的服务不会受到此次更新的影响。"
使用tls解决了http1的两个问题:
1.安全性无法保证
2.单次请求获取一个文档的方式,不满足如今流式传输体验所要求的性能
所以http2中的tls具有流式传输功能的,相比http1.他可以首次建立连接请求,响应返回之后并不会断开连接.他不用向HTTP1那样,每次发送请求都需要建立一次.更重要的是他一次建立连接后,通过这个连接的所有请求都不用等待上一次请求的响应返回,而是可以同时发送请求.
解决批量的方案是:
服务端接收到发送推送消息的请求后,不是马上发送,而是等待100ms,收集所有这段时间的所有推送请求(设置一个上限),一次向一个tls中去推送.
先看下我们的推送架构,基本是根据现有的架构自我调整:
| 消息队列 |
↓
| 消息收集器 |
↓
| 消息合并 |
↓
| 过 滤 器 |
↓
推送模块
1.最上层就不多加说明了,根据自己公司的消息队列获取到要推送的消息.
2.消息收集器是一次获取批量的推送消息,基本公司使用像成熟的消息队列也可以不需要这一层,如果类似使用的redis队列,一次只能获取一个推送消息,就需要加这一层,获取一段时间或者一定量的推送消息,再推给下一层.
for {
//检验是否跳出循环,将数据给到下层
nowTime := time.Now().UnixNano()
if nowTime - beginTime > 150 * millisecond {
if len(p_items) != 0 || len(g_items) != 0 || len(c_items) != 0 || len(s_items) != 0 {
break
}
beginTime = time.Now().UnixNano()
}else if len(p_items) >= 100 {
break
}else if len(g_items) >= 500 {
break
}else if len(c_items) >= 100 {
break
}else if len(s_items) >= 100 {
break
}
/*从队列获取推送消息*/
}
//推给下一层消息收集器
if len(p_items) != 0 {
p_chan<-p_items
}
if len(g_items) != 0 {
g_chan<-g_items
}
if len(c_items) != 0 {
c_chan<-c_items
}
if len(s_items) != 0 {
s_chan<-s_items
}3.消息合并是用来合并群消息,系统消息.适用于同样的内容,推给不同的对象.
for msgs := range g_chan { //这里要小心,不要用到了defer,不然会一直都不释放.
// 合并相同的群消息
var msgs_dict = make(map[string]*group_msg)
/*略*/
for _, gmsg := range group_msgs {
obj, ok := msgs_dict[gmsg.uuid]
if ok {
receivers := obj.receivers
for i := 0; i < len(gmsg.receivers); i++ {
//合并
receivers = append(receivers, gmsg.receivers[i])
}
//保存在map
obj.receivers = receivers
msgs_dict[gmsg.uuid] = obj
} else {
msgs_dict[gmsg.uuid] = gmsg
}
}
/*略*/
}4.过滤器是过滤一些敏感的词语,过滤一些特殊的消息体.
5.推送模块,我们要讲的就是apns2.
获取certificate文件:
func get_apns_cert(P12 string,P12_SECRET string) *tls.Certificate {
cert, err := certificate.FromP12File(P12, P12_SECRET)
if err != nil {
log.Fatal("Cert Error:", err)
return nil
}
return &cert
}获取apns:
func (ap *Apns_push)get_apns_client(appid int64,P12 string,P12_SECRET string) *apns2.Client {
if cer,ok := ap.Apns_cert_dict[appid];ok {
client := ap.Client_manager.Get(cer) //Client_manager -> *apns2.ClientManager
return client
}
cert := get_apns_cert(P12 ,P12_SECRET)
if cert == nil {
return nil
}
ap.Lock()
ap.Apns_cert_dict[appid] = *cert
ap.Unlock()
client := apns2.NewClient(*cert)
ap.Client_manager.Add(client) //Client_manager -> *apns2.ClientManager
return client
}编辑apns消息体:
func(ap *Apns_push)Apns_push(ams []*APNs_msg) {
var notifications []*apns2.Notification
for _,am := range ams {
p := payload.NewPayload()
p.Badge(am.Badge)
p.Alert(am.Content)
if am.Sound == "" {
am.Sound = "default"
}
p.ThreadID(am.Uuid)
p.Sound(am.Sound)
if am.Custom !=nil {
for k,v := range am.Custom {
p.Custom(k,v)
}
}
notification := &apns2.Notification{
DeviceToken:am.DeviceToken,
Topic:am.Bundle_id,
Payload:p,
}
notifications = append(notifications, notification)
}
ap.NotificationsCh<-notifications
}推送:
func send(client *apns2.Client,notification *apns2.Notification,isRepeat bool,seq int64) {
res, err := client.Development().Push(notification)
if err != nil {
log.Warnf("Push fail seq:%d -- %v %v %v token:%s \n",seq, res.StatusCode, res.ApnsID, res.Reason ,notification.DeviceToken)
if isRepeat {
seq++
send(client,notification,false,seq)
}
return
}
log.Infof("Push success seq:%d -- %v %v %v token:%s \n",seq, res.StatusCode, res.ApnsID, res.Reason ,notification.DeviceToken)
}参考资料
Discuss 2018 08 02 Go Shell (`content/discuss/2018-08-02-go-shell.md`)
title: 2018-08-02 Go 调用 shell 脚本如何传递参数 date: 2018-08-02T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-02
问题:Go 调用 shell 脚本如何传递参数?
答:
package main
import (
"fmt"
"os/exec"
)
func main(){
command := "./dir_size.sh" //脚本的路径
cmd := exec.Command("/bin/bash", "-c", command)
output, err := cmd.Output()
if err != nil {
fmt.Printf("Execute Shell:%s failed with error:%s", command, err.Error())
return
}
fmt.Printf("Execute Shell:%s finished with output:\n%s", command, string(output))
}其中 Command 源代码是:
func Command(name string, arg ...string) *Cmd {
cmd := &Cmd{
Path: name,
Args: append([]string{name}, arg...),
}
if filepath.Base(name) == name {
if lp, err := LookPath(name); err != nil {
cmd.lookPathErr = err
} else {
cmd.Path = lp
}
}
return cmd
}其中 name 是执行的命令,args是参数,所以传递参数可以直接写 cmd := exec.Command("/bin/bash", "-c", "参数1","参数","参数") 这样就可以,arg... 为可变参数。
要理解为什么要这么传参,就要理解 bash -c 的用法
bash -c 传参的方法:
用法: bash -c "cmd string"
通常使用 shell 去运行脚本,两种方法 bash xxx.sh,另外一种就是 bash -c "cmd string" 。
对于 bash xxx.sh,首先 bash 会在当前目录去寻找 xxx.sh ,如果找到,就直接运行,找不到则按照环境变量 $PATH 的指定路径,按顺序去找,如果找到,则执行,找不到则报错。
shell 脚本的参数 $0 就是要执行的 shell 脚本 xxx.sh, $1 就是后面紧跟 xxx.sh 的参数,$2 $3依次类推。
而对于 bash -c "cmd string"
-c If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to the positional parameters, starting with $0.
大致意思就是,如果用 -c 那么 bash 会从第一个非选项参数后面的字符串中读取命令,如果字符串有多个空格,第一个空格前面的字符串是要执行的命令,也就是 $0,后面的是参数,即 $1, $2...
参考
Discuss 2018 08 09 Log Color In Go (`content/discuss/2018-08-09-log-color-in-go.md`)
title: 2018-08-09 Log 的颜色能设置吗 date: 2018-08-09T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-09
问题:Log 的颜色能设置吗?
答:可以的。
func TestLogColor(t *testing.T) {
fmt.Println("")
// | 前景 | 背景 | 颜色 |
// |----|----|----|
// | 30 | 40 | 黑色 |
// | 31 | 41 | 红色 |
// | 33 | 42 | 绿色 |
// | 33 | 43 | 黄色 |
// | 34 | 44 | 蓝色 |
// | 35 | 45 | 紫红色 |
// | 36 | 46 | 青蓝色 |
// | 37 | 47 | 白色 |
//
// | 代码 | 意义 |
// |----|----|
// | 0 | 终端默认设置 |
// | 1 | 高亮显示 |
// | 4 | 使用下划线 |
// | 5 | 闪烁 |
// | 7 | 反白显示 |
// | 8 | 不可见 |
for b:= 40; b<=47; b++ {
for f:= 30; f <= 37; f++ {
for d:= range []int{0, 1, 4, 5, 7, 8} {
fmt.Printf(" %c[%d;%d;%dm%s(f=%d,b=%d,d=%d)%c[0m ", 0x1B, d, b, f, "", f, b, d, 0x1B)
}
fmt.Println("")
}
fmt.Println("")
}
fmt.Printf("%c[1;40;32m%s%c[0m", 0x1B, "testPrintColor", 0x1B)
fmt.Println()
fmt.Printf("%s\n", "testPrintColor")
}$ go test -v log_color_test.go对于以上代码的解释:
在其中 \x1b[ 实现 CSI:
转换前景色为黑色,使用 \x1b[30m
转换为红色,使用 \x1b[31m
如果使用加粗参数,灰色写作 \x1b[30;1m
获取红色加粗,使用 \x1b[31;1m
重置颜色为缺省值,使用 \x1b[39;49m (或者使用 \x1b[0m 重置所有属性)
\033[0m 重置为正常
\033[1m 设置高亮度或加粗
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[30m -- /33[37m 设置前景色
\033[40m -- /33[47m 设置背景色控制符 ESC 的常用表示方法 \e、\x1b(\x1B)、\033 都可以 \e 指代 Escape,对应八进制 \033,对应十六进制 \x1b。
参考
Discuss 2018 08 14 Wechat Discuss (`content/discuss/2018-08-14-wechat-discuss.md`)
title: 2018-08-14 做实时语音流,用什么来做比较好 date: 2018-08-14T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-14
问题:做实时语音流,用什么来做比较好?rtmp?还是ws?
- 第三方:即构(推荐)、声网(自己做不稳定,还得搞流媒体服务器)
- 自己做一般都是rtmp
实时?需要有多实时?直播还是点到点通信?
A:有一个实现是在 boya 的 rtmp 协议基础上,完善了 golang 的 rtmp 流媒体服务器,最近已经上网运行了,支持查询回源,定点回源等,资源消耗很低。也可参考项目livego
Go 默认使用 CPU 核数?
具体代码:proc.go#schedinit()。
文档说明:runtime-GOMAXPROCS
Another potentially breaking change is that the runtime now sets the default number of threads to run simultaneously, defined by GOMAXPROCS, to the number of cores available on the CPU. In prior releases the default was 1. Programs that do not expect to run with multiple cores may break inadvertently. They can be updated by removing the restriction or by setting GOMAXPROCS explicitly. For a more detailed discussion of this change, see the design document - Russ Cox.
etcd
etcd 心跳超时是 1s ;
etcd 用了raft 分布式一致性算法。
其他的内容有待后续整理。。。
参考
Discuss 2018 08 15 Wechat Discuss (`content/discuss/2018-08-15-wechat-discuss.md`)
title: 2018-08-15 深度 | 从 Go 高性能日志库 zap 看如何实现高性能 Go 组件 date: 2018-08-15T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-15
深度 | 从 Go 高性能日志库 zap 看如何实现高性能 Go 组件
一篇技术文章引发的讨论。
我们有一个服务用 logrus 落日志,并发量太高了,直接影响了性能。
printf 类型字符串格式化的日志格式,不再推荐,现在日志的消费,逐渐从人转向了计算机,结构化的数据和友好的解析方式更重要。---跟 logrus 的作者观点一样,“核心不是性能”。。。
我们后面也用 sync.Pool 去优化落日志这一块。(当时因为 logrus 写日志这一块,直接在高并发的时候多开销了10G内存,当时服务总共开销才 15 G内存。后面做了一些优化,用 sync.Pool 提前把日志格式化好,不是用 logrus 里面的 WithFields 之类的,后来对比过 zerolog,但是优化后在 3W 左右的日志并发时,从我们服务器的监控来看,整体没有太大的区别(内存、CPU、服务器的整体速度),所以后面也没有更换 log 库。(原因是在于替换成本也比较高。))
Echo 里面的 context 也是用 sync.Pool 解决高并发的时候内存占用问题
sync.Pool 的定位不是做类似于连接池的东西,它的用途仅仅是增加对象重用的几率,减少 gc 的负担,而开销方面也不是很便宜的。
sync.Pool 本来就是当做 cache 用的,在 Go 官方文档上已经明确说明了,数据不能保存在 pool 里面,有可能会遇到 GC 回收。
A Pool is a set of temporary objects that may be individually saved and retrieved.
Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.
参考
- 深度 | 从 Go 高性能日志库 zap 看如何实现高性能 Go 组件
- kingsoft-wps-log4go
- go语言的官方包sync.Pool的实现原理和适用场景
- 大幅提升 golang 写日志序列化性能实践
Discuss 2018 08 23 Wechat Discuss (`content/discuss/2018-08-23-wechat-discuss.md`)
title: 2018-08-23 有什么好的博客平台吗 date: 2018-08-23T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-23
有什么好的博客平台吗?(除了简书)
- Github pages
- hugo + Github
- 自建
- jekyll
- Ghost
- hexo + Github
- hexo + Coding pages
图片CDN:七牛
请问nats有类似[kafka manager](https://github.com/yahoo/kafka-manager)这样的管理后台吗?
- kafka manager 的管理后台:kafkatool
对kafka的抽象
kafka聚焦于数据管道,nats聚焦于message bus
for-select ?
下面这段代码为什么运行一会儿就停止了呢?
用 waitgroup 肯定是正常的,研究一个 go sceduler 的问题,所以才故意这样写的。
for {}这种死循环(现实中应该用不着,反正我没用过这种需求),编译的时候不会被go插入抢占的代码(morestack 函数),导致调度切换不出去。这个问题go团队已经在着手解决了 golang/go/issues#24543,不理解的话,可以看看:
加打印会有系统调用,就会插入调度抢占代码,抢占调度的前提是需要插入morestackt代码,和编译器和调度机制都有关系。。
go 中加入 spinning threads 的目的是啥,有相关资料吗?
让 M 工作。
spin 和 unspin 对应就是 M 运行和休眠的状态,也就是线程运行和休眠的状态,M 只有实在找不多 G 来做的时候才会休眠。
参考
- 系统设计入门
- https://stackshare.io/stackups/kafka-vs-nsq-vs-rabbitmq
- https://github.com/golang/go/issues/15442
- https://rakyll.org/scheduler/
Discuss 2018 08 24 Wechat Discuss (`content/discuss/2018-08-24-wechat-discuss.md`)
title: 2018-08-24 工具推荐 date: 2018-08-24T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-24
工具推荐
- SequelPro(MySQL Client GUI)
- Robot 3T(MongoDB Client GUI)
- mycli(MySQL command line client)
- Navicat Premium(DB Client GUI)
- DataGrip: Database management systems by JetBrains
- SQLyog is an all-round Management Tool (/'GUI'/'Frontend') for MySQL
Discuss 2018 08 30 Understanding Go Interfaces (`content/discuss/2018-08-30-understanding-go-interfaces.md`)
title: 2018-08-30 理解 Go interface date: 2018-08-30T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-08-30
Go 语言不同接口、声明了同名方法,怎么解决问题?
- 防止被其他对象误实现接口。
由此,我陷入了一些思考,所以也引申出来了对 Go 接口实现或者是否这个是一个推荐的做法呢?
网络上的一些讨论[为什么我不喜欢Go语言式的接口(即Structural Typing)](http://blog.zhaojie.me/2013/04/why-i-dont-like-go-style-interface-or-structural-typing.html)
Structural_type_system 是什么?摘自于 wiki
一些讨论。。。
Wuvist 对 Go 语言接口的个人观点
Go里面类适用于哪些接口,不也是程序员显示指定的吗?
区别在于是实现者显示指定,还是调用者显示指定。
我也可以说作为调用者,你非要乱使用一个你不了解的类,那也没人可以拦着你。
但抬杠毫无意义。
值得讨论的,是接口调用者还是接口实现者责任的问题;这才是“Go语言式接口”与常见语言接口的本质不同。
科技球对 Go 语言接口的个人观点
我对“自由严格”之争的基本观点:
第一,我不认为用interface去检查一切能够检查的东西,换句话说,用interface去表达一切library实现者对使用者的约定就是最好的design。我们需要分清楚什么东西是适合用程序语言表达的,什么东西是适合用自然语言表达的,甚至什么东西是适合用数学语言表达的(O(1), O(n)什么的不正是数学语言吗?)。程序语言不适合表达所有的东西,不然我们干嘛需要看编程书,算法书呢?
第二,什么东西适合用程序语言去表达?我觉得程序语言归根结底是人与机器交流的平台,而不是人与人交流的平台。我们之所以提倡静态语言,是为了给计算机表达出足够的信息让计算机能够根据这些信息进行优化,同时利用计算机在编译的时候做一些类似于拼写检查的东西避免人类常犯的错误。但程序不适合用来做人与人交流的平台,哪怕是程序员与程序员交流的平台,与其设计一种语言去表达什么O(1),O(n),thread-safety之类的信息,何不更简单地在注释或者文档里写清楚?同理,技术上的检验也不是越多越好,拼写错误这些是人类常犯的错误,但是high-level的概念上的错误,比如送画画的小明去决斗却是程序员本身就不应该犯的错误。用interface这种程序语言去表达编程的思想,最终的结果就是编程的思想禁锢在程序语言设计者制定的牢笼里。
...
参考资料
- A Tour of the Go Programming Language with Russ Cox
- 深入理解 interface
- Understanding Go Interfaces - YouTube
- Understanding Go Interfaces - Slide
- Go语言中隐式接口的冲突问题
- 《Go语言高级编程》- 接口
- 理解 Go interface 的 5 个关键点
Discuss 2018 09 04 Wechat Discuss (`content/discuss/2018-09-04-wechat-discuss.md`)
title: 2018-09-04 微信讨论 date: 2018-09-04T00:00:00+08:00
来源:『Go 夜读』微信群
Request Body 的请求中,不能 bind 两次吗?
var body struct {
Fcid string "fcid"
}
c.Bind(&body)
log.Traceln("body", body)
var body2 struct {
Fcid string "fcid"
}
c.Bind(&body2)
log.Traceln("body2", body2)打印输出结果为:
2018/09/04 14:58:44.641654 [TRC] jwtValidator.go:34: body {@sign-test2}
2018/09/04 14:58:44.641674 [TRC] jwtValidator.go:41: body2: {}Body 不是 is.Seeker 无法 seek,应该不能重复 bind 的。
protobuf 3 枚举第一个必须是0,但是用的时候,用第一个 struct 会是空
log.Println(protocol.RESULT_CODE)
xxx := &protocol.XResp{
Result: protocol.RESULT_CODE,
}
log.Println(xxx)
// OUTPUT:
// 2018-09-06 09:06:00.111 [d] CODE
// 2018-09-06 09:06:00.111 [d]第一位是占位用的。
参考资料
Discuss 2018 09 05 Git System (`content/discuss/2018-09-05-git-system.md`)
title: 2018-09-05 微信讨论 date: 2018-09-05T00:00:00+08:00
来源:『Go 夜读』微信群
[xxxxx [1 2 3 4]] 如何做到输出为[ssss 1 2 3 4]
func main() {
MutilParam("ssss", 1, 2, 3, 4) //[ssss 1 2 3 4]
iis := []int{1, 2, 3, 4}
MutilParam("xxxxx", iis) //MutilParam= [xxxxx [1 2 3 4]] 如何做到输出为[xxxx 1 2 3 4]
}
func MutilParam(p ...interface{}) {
fmt.Println("MutilParam=", p)
}考点:函数变参
这样的情况会在开源类库如xorm升级版本后出现Exce函数不兼容的问题。
解决方式有两个:
//方法一:interface[]
tmpParams := make([]interface{}, 0, len(iis)+1)
tmpParams = append(tmpParams, "ssss")
for _, ii := range iis {
tmpParams = append(tmpParams, ii)
}
MutilParam(tmpParams...)
//方法二:反射
f := MutilParam
value := reflect.ValueOf(f)
pps := make([]reflect.Value, 0, len(iis)+1)
pps = append(pps, reflect.ValueOf("ssss"))
for _, ii := range iis {
pps = append(pps, reflect.ValueOf(ii))
}
value.Call(pps)实际上都是一样原理的,也很简单的方式处理了。
git 系统平台的选择?
git 系统都有哪一些?
- Github
- Gitlab
- gogs
- Gitea
- Bitbucket
- 码云
- Coding
- ?
更多讨论:
- Gitlab 资源占用太夸张了,但是确实 Gitlab 做的比较好。
云服务器至少得 4核8G
- gogs 有一个超级大的痛点:CodeReview,但是优点也很突出:速度快,资源占用少。
无法选中一行代码发表评论/注释。
- Coding 企业应该都不太敢使用吧。
时序序列告警框架
bosun 是 Time Series Alerting Framework,比较轻量级,但是文档严重滞后。
Prometheus 是用 opentsdb,有点重,还要配上 hbase,玩不转啊。
由于我们是物联网的项目,监控的指标比较多样化,tag的不确定性,应该会导致Prometheus内存耗费很大,而且prometheus采用pull的模式,虽然有pushgateway,还是挺担心的,用了prometheus后,发现业务层的监控代码也会要写的很复杂。
参考资料
- Enum value with index 0 not shown #3808
- proto3#enum
- https://github.com/bosun-monitor/bosun/issues/2301
Discuss 2018 09 14 Tips In Vscode (`content/discuss/2018-09-14-tips-in-vscode.md`)
title: 2018-09-14 VSCode 如何代码自动补全和自动导入包 date: 2018-09-14T00:00:00+08:00
来源:『Go 夜读』微信群
VSCode 如何代码自动补全和自动导入包
VSCode 必须安装以下插件:
首先你必须安装 Golang 插件,然后再给 Go 安装工具包。
在 VS Code 中,使用快捷键:command+shift+P,然后键入:go:install/update tools,将所有 16 个插件都勾选上,然后点击 OK 即开始安装。
Installing 16 tools at /Users/maiyang/develop/goworkspace//bin
gocode
gopkgs
go-outline
go-symbols
guru
gorename
dlv
godef
godoc
goreturns
golint
gotests
gomodifytags
impl
fillstruct
goplay
Installing github.com/mdempsky/gocode SUCCEEDED
Installing github.com/uudashr/gopkgs/cmd/gopkgs SUCCEEDED
Installing github.com/ramya-rao-a/go-outline SUCCEEDED
Installing github.com/acroca/go-symbols SUCCEEDED
Installing golang.org/x/tools/cmd/guru SUCCEEDED
Installing golang.org/x/tools/cmd/gorename SUCCEEDED
Installing github.com/derekparker/delve/cmd/dlv SUCCEEDED
Installing github.com/rogpeppe/godef SUCCEEDED
Installing golang.org/x/tools/cmd/godoc SUCCEEDED
Installing github.com/sqs/goreturns SUCCEEDED
Installing github.com/golang/lint/golint SUCCEEDED
Installing github.com/cweill/gotests/... SUCCEEDED
Installing github.com/fatih/gomodifytags SUCCEEDED
Installing github.com/josharian/impl SUCCEEDED
Installing github.com/davidrjenni/reftools/cmd/fillstruct SUCCEEDED
Installing github.com/haya14busa/goplay/cmd/goplay SUCCEEDED
All tools successfully installed. You're ready to Go :).修改默认配置的方法:
在 Preferences -> Setting 然后输入 go,然后选择
setting.json,填入你想要修改的配置
- 自动完成未导入的包。
"go.autocompleteUnimportedPackages": true,- VSCode 的一些插件需要配置代理,才能够正常安装。
"http.proxy": "192.168.0.100:1087",- 如果你遇到使用标准包可以出现代码提示,但是使用自己的包或者第三方库无法出现代码提示,你可以查看一下你的配置项。
"go.inferGopath": true,- 如果引用的包使用了 ( . "aa.com/text") 那这个text包下的函数也无法跳转进去,这是为什么?
修改 "go.docsTool" 为 gogetdoc,默认是 godoc。
"go.docsTool": "gogetdoc",其他
- 当我们在使用 import 功能的时候,如果无法通过 lint 检查,则不会执行自动 import。
- 如果你需要自动 import 的前提是你必须把要导入的包的函数写完整。
附带我的 settings.json
{
"go.goroot": "",
"go.gopath": "",
"go.inferGopath": true,
"go.autocompleteUnimportedPackages": true,
"go.gocodePackageLookupMode": "go",
"go.gotoSymbol.includeImports": true,
"go.useCodeSnippetsOnFunctionSuggest": true,
"go.useCodeSnippetsOnFunctionSuggestWithoutType": true,
"go.docsTool": "gogetdoc",
}参考资料
- GOPATH in the VS Code Go extension
- VSCode Golang 开发配置之代码提示
- Use gogetdoc instead of godef and godoc #622
Discuss 2018 09 18 Benchmark Tools (`content/discuss/2018-09-18-benchmark-tools.md`)
title: 2018-09-18 压测接口性能的框架,大家有没有推荐 date: 2018-09-18T00:00:00+08:00
来源:『Go 夜读』微信群
压测接口性能的框架,大家有没有推荐?
Discuss 2018 09 19 Wechat Discuss (`content/discuss/2018-09-19-wechat-discuss.md`)
title: 2018-09-19 微信讨论 date: 2018-09-19T00:00:00+08:00
来源: Wechat discuss
时间:2018-09-19
Producer to Consumer via channel
这段code有问题,可能程序跑完了,buffer中还存在数据,就丢失了。
package main
import (
"fmt"
)
const maxBufSize = 3
const numToProduce = 1000
var finishedProducing = make(chan bool)
var finishedConsuming = make(chan bool)
var messageBuffer = make(chan int, maxBufSize)
func produce() {
for i := 0; i < numToProduce; i++ {
messageBuffer <- i
}
finishedProducing <- true
}
func consume() {
for {
select {
case message := <-messageBuffer:
fmt.Println(message)
case <-finishedProducing:
finishedConsuming <- true
return
}
}
}
func main() {
go produce()
go consume()
<-finishedConsuming
fmt.Println("All go routines ended")
}原因
buffer中的消息可能没有被处理
解决办法
应该用for range从channel 里取,produce 那里直接关闭messageChannel就行了。 (AMan)
package main
import (
"fmt"
)
const maxBufSize = 3
const numToProduce = 1000
// var finishedProducing = make(chan bool)
var finishedConsuming = make(chan bool)
var messageBuffer = make(chan int, maxBufSize)
func produce() {
for i := 0; i < numToProduce; i++ {
messageBuffer <- i
}
close(messageBuffer)
}
func consume() {
for message := range messageBuffer {
fmt.Println(message)
}
finishedConsuming <- true
}
func main() {
go produce()
go consume()
<-finishedConsuming
fmt.Println("All go routines ended")
}提升
可以使用 make(chan struct{}) 代替 make(chan bool)
Discuss 2018 09 28 Return Value In Waitgroup (`content/discuss/2018-09-28-return-value-in-waitgroup.md`)
title: 2018-09-28 goroutine 中怎么得到返回值 date: 2018-09-28T00:00:00+08:00
来源: Wechat discuss
时间:2018-09-28
goroutine 中怎么得到返回值?
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
fmt.Println("开始。。。")
wg.Add(2)
time1 := time.Now()
go f1(1, 4)
go f2(2, 5)
wg.Wait()
time2 := time.Since(time1)
fmt.Printf("cost:%v\n", time2)
}
func f1(n1, n2 int) int {
time.Sleep(time.Second)
wg.Done()
fmt.Println("f1")
return n1 + n2
}
func f2(n1, n2 int) int {
time.Sleep(time.Second)
wg.Done()
fmt.Println("f2")
return n1 + n2
}如果我想要获取 f1 ,f2 的返回值,有什么办法?
解决方案1
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
fmt.Println("开始。。。")
wg.Add(2)
time1 := time.Now()
var x int
go func() {
x = f1(1, 4)
wg.Done()
}()
var y int
go func() {
y = f2(2, 5)
wg.Done()
}()
wg.Wait()
fmt.Printf("f1:%d\n", x)
fmt.Printf("f2:%d\n", y)
time2 := time.Since(time1)
fmt.Printf("cost:%v\n", time2)
}
func f1(n1, n2 int) int {
time.Sleep(time.Second)
fmt.Println("f1")
return n1 + n2
}
func f2(n1, n2 int) int {
time.Sleep(time.Second)
fmt.Println("f2")
return n1 + n2
}这里需要注意的地方就是,我们要读取值,必须等待 goroutine 执行完之后才可以。
解决方案2
WaitGroup+Channel 结合的方式:
package main
import (
"fmt"
"sync"
"time"
)
// R return value
type R struct {
name string
ret int
}
func main() {
fmt.Println("开始。。。")
time1 := time.Now()
var wg sync.WaitGroup
retCh := make(chan *R, 2)
wg.Add(1)
go func() {
retCh <- f1(1, 4)
wg.Done()
}()
wg.Add(1)
go func() {
retCh <- f2(2, 5)
wg.Done()
}()
wg.Wait()
close(retCh)
fmt.Println("after wait ...")
for r := range retCh {
fmt.Printf("func name:%s, return:%d\n", r.name, r.ret)
}
time2 := time.Since(time1)
fmt.Printf("cost:%v\n", time2)
}
func f1(n1, n2 int) *R {
time.Sleep(time.Second)
fmt.Println("f1")
return &R{
name: "f1",
ret: n1 + n2,
}
}
func f2(n1, n2 int) *R {
time.Sleep(time.Second)
fmt.Println("f2")
return &R{
name: "f2",
ret: n1 + n2,
}
}参考资料
A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.
A WaitGroup must not be copied after first use.
Discuss 2018 10 18 EncOp (`content/discuss/2018-10-18-encOp.md`)
title: 2018-10-18 encOp 是在哪里实现的呢? date: 2018-10-18T00:00:00+08:00
来源: Wechat discuss
encOp 是在哪里实现的呢?
encode.go文件中的这行代码是对数据进行编码么,具体怎么实现的,在哪里知道么?
instr.op(instr, state, reflect.Value{})
func (enc *Encoder) encode(b *encBuffer, value reflect.Value, ut *userTypeInfo) {
defer catchError(&enc.err)
engine := getEncEngine(ut, nil)
...
}点击进入 getEncEngine -> buildEncEngine -> compileEnc -> encOpFor
func encOpFor(rt reflect.Type, inProgress map[reflect.Type]*encOp, building map[*typeInfo]bool) (*encOp, int) {
...
var op encOp
if int(k) < len(encOpTable) {
op = encOpTable[k]
}
...
}
...
var encOpTable = [...]encOp{
reflect.Bool: encBool,
reflect.Int: encInt,
reflect.Int8: encInt,
reflect.Int16: encInt,
reflect.Int32: encInt,
reflect.Int64: encInt,
reflect.Uint: encUint,
reflect.Uint8: encUint,
reflect.Uint16: encUint,
reflect.Uint32: encUint,
reflect.Uint64: encUint,
reflect.Uintptr: encUint,
reflect.Float32: encFloat,
reflect.Float64: encFloat,
reflect.Complex64: encComplex,
reflect.Complex128: encComplex,
reflect.String: encString,
}点击每个 value 就可以查看其实现了。
Discuss 2018 11 08 Address Operators (`content/discuss/2018-11-08-address_operators.md`)
title: 2018-11-08 Address operators date: 2018-11-08T00:00:00+08:00
来源: Wechat discuss
Address_operators
package main
import (
"fmt"
"math"
)
type abser interface {
Abs() float64
}
type onef float64
func (one onef) Abs() float64{
return math.Pi
}
func main(){
var a,b abser
a = &onef(3.14)
aa := onef(3.14)
b = &aa
fmt.Println(a.Abs())
fmt.Println(b.Abs())
}为什么 a = &onef(3.14) 会报错,而 aa := onef(3.14) 是可以的呢?
For an operand x of type T, the address operation &x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. If the evaluation of x would cause a run-time panic, then the evaluation of &x does too.
对于类型为 T 的操作数 x,地址操作 &x 生成类型为 *T 到 x 的指针。操作数必须是可寻址的,即,变量,指针间接或切片索引操作;
或可寻址结构操作数的字段选择器;
或者可寻址数组的数组索引操作。
作为可寻址性要求的例外,x也可以是(可能带括号的)复合文字。
如果x的评估会导致运行时恐慌,那么&x的评估也会发生。
For an operand x of pointer type *T, the pointer indirection *x denotes the variable of type T pointed to by x. If x is nil, an attempt to evaluate *x will cause a run-time panic.
对于指针类型* T的操作数x,指针间接* x表示由x指向的类型T的变量。如果x为nil,则尝试评估* x将导致运行时出现紧急情况。
参考资料
Discuss 2018 11 08 Aws Ec2 Ssh Login Problem (`content/discuss/2018-11-08-aws-ec2-ssh-login-problem.md`)
title: 2018-11-08 aws ec2实例无法连接登录问题 date: 2018-11-08T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-11-08
aws ec2实例无法连接登录问题
在AWS上注册一个账号,然后创建一个免费试用期限的虚拟机,虚拟机成功创建并启动之后,我们通过SSH登录这台虚拟机出现无法连接的问题.
连接方式aws console页面会提示你如何连接,如下:
# 1. 创建虚拟机的时候会让你生成一个pem秘钥文件作为登录认证,把这个xxx.pem文件放到自己的机器上并设置400权限
chmod 400 wpc-secret.pem
# 2. 登录(注意我的是aws虚拟机是centos系统需要ec2-user用户登录,加-v参数显示登录过程详细信息)
ssh -i "wpc-secret.pem" [email protected] -v
# 3. 我们会发现无法登录,超时登录后断开了,是因为默认ec2上面创建的虚拟机在默认情况下,默认安全组不允许传入SSH流量。所以你需要在ec2 console中的左侧菜单栏找到安全组然后添加SSH类型的入站和出站规则即可
略...
# 4. 重新尝试登录,一般情况先就成功登录上了
ssh -i "wpc-secret.pem" [email protected] -v
...Discuss 2018 11 09 Force To Use Keyed Struct Literals (`content/discuss/2018-11-09-force-to-use-keyed-struct-literals.md`)
title: 2018-11-09 强制使用字段命名方式初始化结构体 date: 2018-11-09T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-11-09
强制使用字段命名方式初始化结构体
在定义结构体时,加入一个非导出且大小为0的字段, 编译器会强制开发者使用字段命名的方式初始化结构体, 而不能按顺序来赋值.
这个小技巧利用了包的非导出字段可见性, 所以只能在不同包下初始化结构体才有用.
// foo.go
package foo
type Config struct {
_ [0]int
Name string
Size int
}// main.go
package main
import "foo"
func main() {
//_ = foo.Config{[0]int{}, "bar", 123}
// doesn't compile
// 报错信息:implicit assignment of unexported field 'flag' in foo.Config literal
_ = foo.Config{Name: "bar", Size: 123} // compile okay
}不要把非导出且大小为0的字段放在结构体的最后面, 可能会导致多分配内存.
参考资料
- How to force package users to use struct composite literals with field names?
- Why does the final field of a zero-sized type in a struct contribute to the size of the struct sometimes?
- golang 内存分析之字节对齐规则
Discuss 2018 11 29 Config In Go (`content/discuss/2018-11-29-config-in-go.md`)
title: Golang 用什么配置中心管理服务 date: 2018-11-29T17:13:46+08:00
来源:『Go 夜读』微信群
时间:2018-11-29
Golang 用什么配置中心管理服务?
- etcd
- Qconf
Discuss 2018 12 04 Change To Go (`content/discuss/2018-12-04-change-to-go.md`)
title: 实打实的 Go 项目,但是 Github 却统计为 JavaScript 怎么办? date: 2018-12-04T17:13:46+08:00
来源:『Go 夜读』微信群
时间:2018-12-04
原理
Github 采用 Linguist 来自动识别你的代码,然后判断属于哪一种语言。
问题
但是我是一个 Go 相关的文档或者 Go Web 工程项目,肯定希望在搜索 Go 语言的时候,也能包括我,并且显示项目所属语言为 Go。
但是默认情况下,你肯定会失望。。。可能还会吐槽:为什么 Github 不提供一个选择项目所属语言呢?
针对这个问题,我的思考是,Github 就是想让大家 hack 一点。
解决方法
- 在项目中增加更多的你所想要归属的语言的代码(综合性项目,最好是采用这种方式)。
- 在项目中添加
.gitattributes
*.js linguist-language=go
*.java linguist-language=go
...这种方式会导致你处理的后缀语言也不会在项目中被标识出来了。
Discuss 2018 12 04 Wechat Discuss (`content/discuss/2018-12-04-wechat-discuss.md`)
title: "2018-12-04 微信讨论" date: 2018-12-04T17:13:46+08:00
来源:『Go 夜读』微信群
json 解析结构体断言问题
package main
import (
"encoding/json"
)
type Msg struct {
Id int `json:"id"`
Params []interface{} `json:"params"`
}
type Employee struct {
Name string `json:"name"`
}
func main() {
b := []byte(`{"id":111,"params":["boss",[{"name":"a"},{"name":"b"}]]}`)
msg := &Msg{}
json.Unmarshal(b, msg)
_ = msg.Params[0].(string)
_ = msg.Params[1].([]Employee) //无法直接断言?
}对于复杂多变的结构体, 经常会用 interface 作为类型,然后根据情况断言
上面的例子运行起来会报错, 原因是 json.Unmarshal 之后无法再断言成具体的结构体 (虽然结构体和 map[string]interface{}可以互转)
解决方案1️⃣(非最优):
把想要断言成具体 struct 的字段重新json.Marshal 和 json.Unmarshal,会多操作一步不推荐
newB, _ := json.Marshal(msg.Params[1])
var employee []Employee
json.Unmarshal(newB, &employee)解决方案2️⃣:
把 interface{}类型改为 json.RawMessage ,可以延迟该字段的解析, 在用到的时候进行二次解析
package main
import (
"encoding/json"
"log"
)
type Msg struct {
Id int `json:"id"`
Params []json.RawMessage `json:"params"`
}
type Employee struct {
Name string `json:"name"`
}
func main() {
b := []byte(`{"id":111,"params":["boss",[{"name":"a"},{"name":"b"}]]}`)
msg := &Msg{}
json.Unmarshal(b, msg)
var boss string
json.Unmarshal(msg.Params[0], &boss)
log.Println(string(boss))
var employee []Employee
json.Unmarshal(msg.Params[1], &employee)
log.Println(employee)
}Discuss 2018 12 07 Wechat Discuss (`content/discuss/2018-12-07-wechat-discuss.md`)
title: "2018-12-07 微信讨论" date: 2018-12-07T23:13:46+08:00
来源:『Go 夜读』微信群
应该还是负数问题,你实验一下
下面这种情况的负数报错是在 build 阶段报的:
errMake := make([]byte, 64-22*5) // negative len argument in make([]byte)下面这种情况的负数,编译时不知道,是在运行时的报错:
errMake := make([]byte, 64-len(testStr)*5) // runtime error: makeslice: len out of rangeDiscuss 2018 12 11 Wechat Discuss (`content/discuss/2018-12-11-wechat-discuss.md`)
title: "2018-12-11 微信讨论" date: 2018-12-11T00:00:00+08:00
来源:『Go 夜读』微信群
时间:2018-12-11
Golang module初始化
使用go mod命令进行初始化:
go mod init [module name]如:go mod init cmpp
Golang module文件
调用go mod init会自动生成go.mod文件,使用如下:
module tx
require github.com/sirupsen/logrus v1.2.0
require (
github.com/acroca/go-symbols v0.0.0-20180523203557-953befd75e2 // indirect
github.com/ramya-rao-a/go-outline v0.0.0-20181122025142-7182a932836a // indirect
github.com/rogpeppe/godef v1.1.0 // indirect
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect
github.com/stamblerre/gocode v0.0.0-20181212030458-2f9d39d8f31d // indirect
)
replace (
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 => github.com/golang/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/net v0.0.0 => github.com/golang/net v0.0.0-20181207154023-610586996380 // indirect
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 => github.com/golang/sys v0.0.0-20181213200352-4d1cda033e06 // indirect
golang.org/x/text v0.0.0 => github.com/golang/text v0.3.1-0.20181211190257-17bcc049122f // indirect
golang.org/x/tools v0.0.0-20181130195746-895048a75ecf => github.com/golang/tools v0.0.0-20181213210126-fe2443f7b950 // indirect
)indirect的意思是指这个package被子module/package依赖了,但是main module并没有直接import使用,也就是所谓的间接引用。
mod版本号解读
github.com/acroca/go-symbols v0.0.0-20180523203557-953befd75e2含义:
前面部分是包的名字,也就是import时需要写的部分,而空格之后的是版本号,版本号遵循如下规律:
vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
vX.0.0-yyyymmddhhmmss-abcdefabcdef
vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
vX.Y.Z
也就是版本号+时间戳+hash,我们自己指定版本时只需要制定版本号即可,没有版本tag的则需要找到对应commit的时间和hash值。
默认使用最新版本的package。
这个组合的版本号可称为伪版本号。
总不能让大家记住每个包的伪版本号吧?显然不太现实,初始化mod文件可以如下写:
require github.com/sirupsen/logrus latest然后运行 go run main.go ,会主动更新版本号,如果运行出现提示不支持latest,可将latest更改为master。
注:如果的确获取不到版本号,实在不知道的话(比如需翻墙才可获取的包),就试试v0.0.0
mod文件格式化
使用如下命令:
go mod edit -fmt1.会删除冗余的类库
2.自动格式化,并排序类库
注:mod文件可以有多个require,括号要与关键词隔开。
问题一:Golang module下golang.org如何处理被墙
系统提示
go: golang.org/x/[email protected]: unrecognized import path "golang.org/x/sys" (https fetch: Get https://golang.org/x/sys?go-get=1: dial tcp 216.239.37.1:443: i/otimeout)
go: golang.org/x/[email protected]: unrecognized import path "golang.org/x/crypto" (https fetch: Get https://golang.org/x/crypto?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
go: error loading module requirements解决方案
在go.mod进行如下设置:
replace (
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
golang.org/x/sys v0.3.0=>github.com/golang/sys v0.3.0
)问题二:初始化权限设置
系统提示
go: writing stat cache:, permission denied解决方案
sudo chown -R $(whoami):admin /Users/zhushuyan/go/pkg && sudo chmod -R g+rwx /Users/zhushuyan/go/pkgDiscuss 2018 12 25 MacOS Time Synchronization (`content/discuss/2018-12-25-macOS-time-synchronization.md`)
title: 2018-12-25 MacOS Time Synchronization date: 2018-12-25T16:36:45+08:00
来源: Wechat discuss
MacOS Time Synchronization
今天在启动 Geth (以太坊私链节点)的时候,出现了以下的错误。在比较后发现本机时间落后了北京时间近一分钟。
在搜索解决方案的时候,发现在 macOS Mojave 里面
ntpdate已经废弃了。
改用了sntp,在Terminal输入以下的命令,即可同步。
sudo sntp -sS time.asia.apple.com系统设置建议
- 1、自动设置日期与时间
- 2、时区不要自动设定,选择中国北京,因为不同地区的时间会有误差。
参考资料
Discuss 2019 01 10 Anlayze Range (`content/discuss/2019-01-10-anlayze-range.md`)
title: 2019-01-10 关于range的一些不易察觉的"坑" date: 2019-01-10T00:00:00+08:00
来源: Wechat discuss
1. 以下代码最终的结果是什么?
func main(){
sli := []int{6,7,8}
for i := range sli {
sli = append(sli,666)
fmt.Println(i)
}
}
// Output:面试时遇到的一个问题,
这段代码会形成死循环吗?
回来之后试了一下出乎意料:
输出结果为:
0
1
2看上去 for 循环的次数,在进入循环体前已经确定了,且次数为 range 后 len(sli)
实际上,range 为 golang 的语法糖,其实际执行相当于
func main() {
sli := []int{6,7,8}
len_sli := len(sli)
for index := 0 ; index < len_sli; index++ {
sli = append(sli,666)
fmt.Println(index)
}
}即在进入循环之前,控制循环次数的这个 len_sli 参数已经确定;
在循环体内对原切片进行 append 操作,并不会影响 len_sli 的值
2. 写出以下代码的输出:
package main
const N = 3
func main(){
m := make(map[int]*int)
for i := 0; i < N; i++ {
m[i] = &i
}
for _, v := range m {
print(*v)
}
}初步分析:
- 在第一个循环中为m赋值,键名0,1,2分别对应着键值0,1,2的内存地址
- 在第二个循环中迭代m,每次循环用*取出指针键值对应内存地址里存的值
- 初步分析,结果应该是0,1,2
运行结果为:
3
3
3结果却是3,3,3
- 我们加一下注释代码再来看:
func main() {
m := make(map[int]*int)
for i := 0; i < 3; i++{
m[i] = &i //A
fmt.Println("&i的值是:",&i)
fmt.Println("i的值是:",i)
}
for c,v := range m {
fmt.Println(c)
time.Sleep(1e9)
fmt.Println(*v)
time.Sleep(1e9)
}
}结果如下:
&i的值是: 0xc420016468
i的值是: 0
&i的值是: 0xc420016468
i的值是: 0
&i的值是: 0xc420016468
i的值是: 0
0
3
1
3
2
3即在迭代中m的三个元素的指针相同,都指向了最后一个迭代对象的地址,在此即3的值
如果在迭代体中需要访问数组/map元素的指针,那么务必小心.这类 bug 无形极难轻易寻获
改进办法:引入中间变量,如下:
func main(){
m := make(map[int]*int)
for i := 0; i < 3; i++ {
x := i
fmt.Println(x)
fmt.Println(&x)
m[i] = &x
fmt.Println("&i的值是:",&i)
fmt.Println("i的值是:",i)
}
for c,v := range m {
fmt.Println(c)
time.Sleep(1e9)
fmt.Println(*v)
time.Sleep(1e9)
}
}输出为:
0
0xc420016470
&i的值是: 0xc420016468
i的值是: 0
1
0xc420016490
&i的值是: 0xc420016468
i的值是: 1
2
0xc4200164a8
&i的值是: 0xc420016468
i的值是: 2
0
0
1
1
2
2- 即此处v其实是一个全局变量,只分配了一次内存地址
总结:
关于range,有两点需要注意:
- 一个是长度在循环之前就已经确定
- 另一个是迭代出的值是全局变量~
Discuss 2019 02 20 Bigdecimal Show String (`content/discuss/2019-02-20-bigdecimal-show-string.md`)
title: 2019-02-20 浮点数如何输出 date: 2019-02-20T00:00:00+08:00
来源: Wechat discuss
将数据序列化为json的时候,怎么让序列化后的 json 里面不要使用科学计数法
json.Marshal 转出来的json怎么能不让转成科学计数法 大家有解决办法吗?
dec := decimal.NewFromFloat(0.000001)
fmt.Println(dec.String())参考资料
Discuss 2019 03 07 Wechat Discuss (`content/discuss/2019-03-07-wechat-discuss.md`)
title: "2019-03-07 微信讨论" date: 2019-03-07T10:51:00+08:00
来源:『Go 夜读』微信群
1 Goland中println和fmt.Print乱序原因
Question: 下面的代码,在goland上运行后,为何输出顺序不确定?但是使用命令行go run xxx运行,输出顺序却是确定的?
package main
import "fmt"
func main() {
println("hello")
println("world")
fmt.Print("go\n")
}
输出结果:
hello
world
go
或是:
go
hello
worldAnswer:
println输出流是stderr,而fmt.Print输出流是stdout。而goland应该是区分stderr和stdout的(输出结果中stderr为红色),这种顺序不确定应该是goland自身处理stderr/stdout输出的时候导致的。因此通过非goland(比如console)方式运行该代码,顺序是确定的。
reference:
2 Go 终端输出彩色文字方法
2.1 色彩编码
ANSI转义序列是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置/颜色和其他选项。在文本中嵌入确定的字节序列,大部分以ESC转义字符和[字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。
其中有专门控制字符颜色的控制符。一般由ESC[开始,中间包含若干个(包括0个)参数字节,以及一个最终字节组成。例如:\x1b[37;44;4;1m hello go \x1b[0m,代表输出的文字hello go的格式是:蓝色背景(44),灰色字体(37),带下滑下(4)并且加粗(1),go 语言代码如下:
package main
import "fmt"
func main() {
fmt.Printf("\x1b[37;44;4;1m hello go \x1b[0m")
}其中:
- \x1b 标志字符,代表转义序列开始,其实就是0x1B
- [ 转义序列的开始符
- 以 ; 分割的数字是控制字符
- m 代表结束控制字符序列常用的文本样式控制符如下:
| 编码 | 说明 |
|---|---|
| 0 | 重置/清除样式 |
| 1 | 加粗 |
| 3 | 斜体 |
| 4 | 下划线 |
| 5 | 闪烁 |
| 8 | 隐藏 |
| 30~37 | 前景色,参考下文『1位颜色编码』 |
| 38 | 设置前景色,后跟 5;n 代表使用8位256颜色码,后跟 2;r;g;b代表24位RGB颜色码 |
| 40~47 | 背景色,参考下文『1位颜色编码』 |
| 48 | 设置背景色,后跟 5;n代表使用8位256颜色码,后跟 2;r;g;b代表24位RGB颜色码 |
| 90~97 | 亮色前景色,参考下文 『1 位颜色编码』 |
| 100~107 | 亮色背景,参考下文 『1 位颜色编码』 |
1位颜色编码
| 颜色 | 前景色编码 | 背景色编码 |
|---|---|---|
| 黑色 | 30 | 40 |
| 红色 | 31 | 41 |
| 绿色 | 32 | 42 |
| 黄色 | 33 | 43 |
| 蓝色 | 34 | 44 |
| 品红色 | 35 | 45 |
| 青色 | 36 | 46 |
| 白色(灰) | 37 | 47 |
| 亮黑色(灰) | 90 | 100 |
| 亮红色 | 91 | 101 |
| 亮绿色 | 92 | 102 |
| 亮黄色 | 93 | 103 |
| 亮蓝色 | 94 | 104 |
| 亮品红色 | 95 | 105 |
| 亮青色 | 96 | 106 |
| 亮白色 | 97 | 107 |
2.2 例子
通过颜色控制,我们可以输出带颜色的log,例子如下:
package main
import (
"fmt"
"time"
)
const (
color_red = uint8(iota + 91)
color_green
color_yellow
color_blue
color_magenta
succ = "[succ]"
error = "[error]"
warn = "[warn]"
info = "[info]"
debug = "[debug]"
)
func red(s string) string {
return fmt.Sprint("\x1b[%dm%s\x1b[0m", color_red, s)
}
func green(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color_green, s)
}
func yellow(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color_yellow, s)
}
func blue(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color_blue, s)
}
func magenta(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color_magenta, s)
}
func Success(format string, a ... interface{}) {
prefix := green(succ)
fmt.Println(formatLog(prefix), fmt.Sprintf(format, a ...))
}
func Error(format string, a ... interface{}) {
prefix := red(error)
fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...))
}
func Warning(format string, a ... interface{}) {
prefix := magenta(warn)
fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...))
}
func Info(format string, a ... interface{}) {
prefix := blue(info)
fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...))
}
func Debug(format string, a ... interface{}) {
prefix := yellow(debug)
fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...))
}
func formatLog(prefix string) string {
return time.Now().Format("2006/01/02 15:04:05") + "" + prefix + ""
}
func main() {
Debug("%s", "hello, go")
}2.3 参考
Discuss 2019 03 08 Wechat Discuss (`content/discuss/2019-03-08-wechat-discuss.md`)
title: "2019-03-08 微信讨论" date: 2019-03-08T21:00:00+08:00
读写锁引入
有下面一段程序,面试官问这段程序有什么问题?
type Store struct {
a string
b string
sync.RWMutex
}
func (s *Store) GetA() string {
fmt.Println("get a")
s.RLock()
fmt.Println("get a2")
defer s.RUnlock()
return s.a
}
func (s *Store) GetAB() (string, string) {
fmt.Println("get ab")
s.RLock()
fmt.Println("get ab2")
defer s.RUnlock()
return s.GetA(), s.b
}
func (s *Store) Write(a, b string) {
fmt.Println("write")
s.Lock()
defer s.Unlock()
fmt.Println("write2")
s.a = a
s.b = b
}- 看到这段程序程序,首先想到的是读写锁的问题;
- 其次,看 Store 这个结构体,各个函数都定义的是指针函数。那就说明:不存在读写锁的 copy 过程;
- GetAB 方法中通过调用 GetA 方法,在
s.RUnlock前通过调用s.GetA,又做了一次读写锁上锁s.RLock,但是读锁可以多次上锁,所以单看这里没什么问题; - 然后,想到会不会
Write和GetAB并发调用的时候会存在问题呢?思考了一会,觉得没问题,就放弃了。
以上,是面试时整个思路。
回头,越想越觉得这里哪里有问题,就在夜读群里求教了一下,群里大神发了一篇读写锁优先级的文章,然后给了一段测试样例,瞬间豁然开朗。
main函数逻辑如下:
func main() {
store := Store{}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for i := 1; i < 10000; i += 1 {
fmt.Println("main write ", i)
store.Write("111", "1111")
}
}()
go func() {
defer wg.Done()
for i := 1; i < 10000; i += 1 {
fmt.Println("main get ab", i)
store.GetAB()
}
}()
wg.Wait()
}执行结果
main get ab 12 //main函数读取ab
get ab //进入 s.GetAB 函数
main write 1 //main 函数写数据
write //进入 s.Write 函数
write2 //获取写锁
main write 2
.... //写锁一直抢占
....
main write 13 //main 函数写数据
write //进入 s.Write 函数
write2 //获取写锁
main write 14
write
write2
get ab2 //之前 get ab 12 才获得读锁
get a //进入 GetA
get a2 //获取读锁
main get ab 13 //main函数 get ab
get ab //进入 s.GetAB 函数
get ab2 //获取读锁
main write 15 //注意⚠️ 这个时候写数据开始了
write //进入 Write 函数,后面尝试获取写锁
get a //这个时候 GetAB 进入了 GetA,尝试获取读锁
fatal error: all goroutines are asleep - deadlock! //出现了死锁分析:
GetAB | GetA | Write
| |
r0 占用读锁 | |
| | w0 尝试获取写锁 等待r0释放读锁
| r1 尝试获取读锁,排在w0后面 |由于读写锁的优先级,读锁和写锁同时竞争时,读锁要排在写锁后面,导致了 r1 竞争 w0的锁,w0竞争r0,r0执行不下去,最后死锁。
读写锁底层
读写锁前置条件:
- 读写互斥,但是读读不互斥;
- 读、写锁都不会出现饥饿;
- 保证读上锁数量与解锁数量一致;
可以思考下,如果让你设计一个这样的锁,你会怎么设计?
go中读写锁的结构,如下:
type RWMutex struct {
w Mutex // 用来保证同一时间只有一个写锁能够抢到锁
writerSem uint32 // 写锁信号量,在读锁全部解锁时通知阻塞的写锁
readerSem uint32 // 读锁信号量,在写锁解锁时通知阻塞的读操作
readerCount int32 // 等待、已上锁的读锁数量
readerWait int32 // 写锁获得锁前,已经上锁的读锁数量
}读锁逻辑
首先,看一下读上锁逻辑:
func (rw *RWMutex) RLock() {
...
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false)
}
...
}上面,上读锁逻辑获试图获取读锁数量原子性加一: atomic.AddInt32(&rw.readerCount, 1)。自增操作返回值如果小于0,则阻塞等待信号量 readerSem 唤醒。
疑问:
- 什么情况下
readerCount小于0; runtime_SemacquireMutex不会造成读读互斥么?- 如何保证读、写互斥?
再来看一下,读解锁逻辑:
func (rw *RWMutex) RUnlock() {
...
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
throw("sync: RUnlock of unlocked RWMutex")
}
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false)
}
}
...
}解锁逻辑:先对 atomic.AddInt32(&rw.readerCount, -1) 进行原子性减一操作。
- r大于 0 :直接释放锁完成;
- r小于 0 :进行读锁数量一致性判断,
atomic.AddInt32(&rw.readerWait, -1)针对readerWait原子性减一后判断是否为 0,为 0 则唤起写锁信号量;
与读加锁类似,同样有 atomic.AddInt32(&rw.readerCount, -1) 小于 0 判断。可以有结论 rw.readerCount 小于 0,为写锁上锁的充要条件,后面分析写锁时进行验证。
解决了的问题:
- 释放读锁,读锁全部释放后唤起写锁;
- 上锁与解锁数量一致性保证;
疑问:
readerCount修改成一个负数?如何保证这个负数足够小呢?
写锁逻辑
先上代码:
func (rw *RWMutex) Lock() {
...
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false)
}
...
}写上锁逻辑:
- 首先,互斥量上锁,保证只有一个写锁加锁成功。
- 然后,令
readerCount原子性减去rwmutexMaxReaders(这是个常量,具体定义const rwmutexMaxReaders = 1 << 30)。这里可以验证之前猜想,rw.readerCount小于0,是持有锁的充要条件。atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders返回结果是在写锁获取前,已持有读锁的数量 r。- r=0,说明没有读锁;
- r<0,只有在
读解锁数量>读加锁数量,或写锁多次时发生;第一个情况,读解锁会check;第二种情况,mutex保证同时只有一个写锁; - r>0,存在读锁;
- 再进行 r!=0 判断(即存在读锁)。原子性操作
atomic.AddInt32(&rw.readerWait, r),记录需要等待的读锁数量,然后等待writerSem唤醒。
最终,保证:1. 写锁唯一性;2. 等待读锁完全释放;3. 阻塞后面读锁的获取;
再来看一下,写锁解锁逻辑:
func (rw *RWMutex) Unlock() {
...
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
throw("sync: Unlock of unlocked RWMutex")
}
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false)
}
// Allow other writers to proceed.
rw.w.Unlock()
...
}解锁逻辑:
- 原子性操作
atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)。这里,能够看到两个隐含的点:- 原子操作结束后,如果有其他读锁试图获取读锁,不需要阻塞;
- 这个时候其他线程还是不能够获取写锁;
- 即:
写锁释放锁时,读锁要比写锁优先级高;
- 原子操作返回值,是当前读锁数量。包括在写锁前读锁(写锁未完全获得情况下写锁解锁),和写锁后阻塞读锁;然后
runtime_Semrelease唤起阻塞着的读锁。runtime_Semrelease > runtime_SemacquireMutex会不会存在问题?验证过不会。
- 然后写锁释放;
总结
通过分析,可以得出结论:
- 写锁释放过程中,读锁优先级要高于写锁;
- 读锁加锁后,写锁可以进入加锁过程,但是要等待之前读锁释放;即,并不少写锁优先级高于写锁,而是在
读锁已经上锁,或没有持有读写锁的协程条件下,读写锁都有机会获取锁;
所以,针对之前的面试题,读锁嵌套读锁,在有写锁的时候,依据结论2会发生死锁。
通过上面分析,存在待验证问题:
一个协程个已获取读锁,另个协程试图获取写锁,还有一个协程在完全获取写锁前调用Unlock,再一个协程释放读锁,按顺序进行流程。会发生死锁具体可以自己分析(写锁信号量永远阻塞);一个协程已上写锁锁,一个协程试图获取读锁,然后另一个协程释放读锁,最后一个协程释放写锁,同样会发生死锁(读信号量永远阻塞);
在以后用锁的时候不管有没有优先级,都要时刻记住死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 锁的不可抢占:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
参考
- [sync.RWMutex]https://medium.com/golangspec/sync-rwmutex-ca6c6c3208a0
Discuss 2019 03 26 Gopsutil (`content/discuss/2019-03-26-gopsutil.md`)
title: 2019-03-26 微信群讨论 date: 2019-03-26T00:00:00+08:00
来源:『Go 夜读』微信群
时间:未知
背景
先前由于公司这边要做一些基础指标的监控上报,因此之前在夜读群里和大家聊了下。经推荐最后使用了第三方库 gopsutil
今天我把之前查的资料重新翻了出来,大家有兴趣的可以看看。以备不时之需及学习一下
例子
package main
import (
"time"
"log"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
)
type StatusServer struct {
Percent StatusPercent
CPU []CPUInfo
Mem MemInfo
Swap SwapInfo
Load *load.AvgStat
BootTime uint64
Uptime uint64
}
type StatusPercent struct {
CPU float64
Disk float64
Mem float64
Swap float64
}
type CPUInfo struct {
ModelName string
Cores int32
}
type MemInfo struct {
Total uint64
Used uint64
Available uint64
}
type SwapInfo struct {
Total uint64
Used uint64
Available uint64
}
func main() {
v, _ := mem.VirtualMemory()
s, _ := mem.SwapMemory()
c, _ := cpu.Info()
cc, _ := cpu.Percent(time.Second, false)
d, _ := disk.Usage("/")
n, _ := host.Info()
l, _ := load.Avg()
ss := new(StatusServer)
ss.Load = l
ss.Uptime = n.Uptime
ss.BootTime = n.BootTime
ss.Percent.Mem = v.UsedPercent
ss.Percent.CPU = cc[0]
ss.Percent.Swap = s.UsedPercent
ss.Percent.Disk = d.UsedPercent
ss.CPU = make([]CPUInfo, len(c))
for i, ci := range c {
ss.CPU[i].ModelName = ci.ModelName
ss.CPU[i].Cores = ci.Cores
}
ss.Mem.Total = v.Total
ss.Mem.Available = v.Available
ss.Mem.Used = v.Used
ss.Swap.Total = s.Total
ss.Swap.Available = s.Free
ss.Swap.Used = s.Used
log.Printf("Server: %+v", ss)
}输出结果
2019/03/26 20:59:33 Server: &{
Percent:{CPU:46.38403990024938 Disk:77.4866172890781 Mem:67.66278743743896 Swap:95.16252790178571}
CPU:[{ModelName:Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz Cores:2}]
Mem:{Total:17179869184 Used:11624378368 Available:5555490816}
Swap:{Total:7516192768 Used:7152599040 Available:363593728}
Load:{"load1":4.28,"load5":3.37,"load15":3.21}
BootTime:1550416891
Uptime:3188282
}Discuss 2019 04 10 Remove Local Branch In Merged Master (`content/discuss/2019-04-10-remove-local-branch-in-merged-master.md`)
title: 2019-04-10 date: 2019-04-10T00:00:00+08:00
来源:『Go 夜读』微信群
本地开发分支已经 merge 到 master 后,如何快捷的删除本地开发分支?
#!/bin/bash
function git_branch_cleanup() {
for branch in `git branch --format='%(refname:short)'|grep -v '\*\|master'` ; do
git checkout $branch
check_results=`git fetch origin master && git rebase origin/master`
result=$(echo $check_results | grep "up to date.")
if [ "$result" == "" ];then
echo "不包含 up to date. $check_results !\n"
fi
done
git checkout master
git branch --merged | grep -v '\*\|master' | xargs -n 1 git branch -d
}
git_branch_cleanup你可以将以上代码创建到 /usr/local/bin/gcup,这样你就可以在项目中使用 gcup 命令了。
注意:gcup 需要权限:
chmod +x gcup
如果之前没有清理无用分支,可能会有大量冲突需要处理。
其他办法:
我们也可以遍历本地所有分支,然后排除一些白名单的分支,然后再把其他的全部删掉。
git branch -d xxx能删掉的就都删掉了,如果你确定 xxx 分支没有用了,你也可以强制删除git branch -D xxx
更简洁的代码:git branch -d $(git branch -vv | grep ': gone\]' | awk '{print $1}')
Discuss 2019 05 15 Jaeger (`content/discuss/2019-05-15-jaeger.md`)
title: 2019-05-15 date: 2019-05-15T00:00:00+08:00
来源:『Go 夜读』微信群
Q: jaeger好像是由入口服务决策这条trace是否需要采样上报的?所有下游的服务都会根据入口服务的采样标记决定是否将数据上报给agent,这样的话有个问题:
如果我的后端服务架构中,入口服务的请求量特别大,但是最下游的两三个服务请求量相对下(相差100倍以上),那如何妥善地设置采样比例?
比例小的话,下游服务的上报量可能会很少,统计分析的时候准确度不够;
比例大的话,入口服务的上报量又会太大
A: 不太明白:入口和最下游服务不是一起访问?
Q: 不是的,你可以把我们后台看作类似于漏斗形架构,数据每经过一个服务就会做判断,可能需要继续访问下游服务,也可能不需要、直接返回,所以越往下游的服务接受的请求量越小;
每个请求到底会经过哪些服务,是不固定的
依赖于每一层服务的判断结果;
A: 没想到办法[悠闲]
Q: 暂时想到的办法是,每台机的agent单独采样,根据单机请求量来调整上报数据, 这样可以确保每台机器的上报数据量均衡,但是es里最终存储的不是完整trace,完整trace需要单独回访数据
A: agent单独采样:是指使用远程采集策略?
Q: 不是远程策略,远程策略,归根到底也是入口服务做采样决策的,这里需要的是agent来决策采样,不过这样就意味着客户端到agent的数据是全量的,udp传输开销应该会很大,又需要改造为共享内存传输之类的。
A1: 那采样就进程内概率随机采,每台机的agent汇总,agent再做一些可动态调整的limiter呢
A: 不太清楚你的具体实现,但全量上报再由agent决策,好像也解决不了下游服务只有很小访问的问题吧?
Q: 下游服务访问量小,所以下游服务的机器agent就可以根据情况调整采样比例增加数据上报量
A: 意思是不需要看到和入口服务在同一条链路上?
Q: 是的,前面说的,这样的做法就是es存储的span不一定能拼成完整的trace;
Q: 需求上来讲,更需要的是全局视图, 统计整体链路的请求响应情况
需要某个具体case的时候再去回放,带有debug标志的trace的所有span都会上报,可以形成完整链路
A: 单独对你的下游服务设定不同的采集策略不会好点?
Q: 问题不是在于,jaeger是从入口服务进行采样决策的吗
Q: Jaeger libraries implement consistent upfront (or head-based) sampling.
or example, assume we have a simple call graph where service A calls service B, and B calls service C: A -> B -> C. When service A receives a request that contains no tracing information, Jaeger tracer will start a new trace, assign it a random trace ID, and make a sampling decision based on the currently installed sampling strategy. The sampling decision will be propagated with the requests to B and to C, so those services will not be making the sampling decision again but instead will respect the decision made by the top service A.
A: 如果你们能接受不在一条链路上,可以进行不同设定,例如入口使用随机采集策略,几率是(1/1000),那下游服务就(1/100)
Q: “so those services will not be making the sampling decision again but instead will respect the decision made by the top service A”
jaeger下游服务不做采样决策了
A: 如果你想看到在同一条链路上,下游就不需要做决策,但是现在你们可以接受不在一条链路上,那就下游自己做决策
Q: 那下游自己决策,应该需要改动client吧?
A: 不需要啊,你只要不去找爸爸就行了, 不提取上游的传递的数据
Q: 嗯,这也是一种
未梳理,只是讨论纪要。
Discuss 2019 05 21 Chromedp (`content/discuss/2019-05-21-chromedp.md`)
title: 2019-05-21 浏览器自动化工具 date: 2019-05-21T00:00:00+08:00
来源:『Go 夜读』微信群
A0: chromedp 浏览器自动化工具,还有 puppeteer。
A1: chromedp 不如 github.com/raff/godet
chromedp 到现在都没把 chrome console 的功能实现
我们做 cookie scanner 的时候把相关的chrome driver 库用了个遍。
我们这里已经都测过了 https://github.com/CovenantSQL/CookieScanner
A2: puppeteer 很好啊
A3: chromedp 用起来比较难受,特别是部署的时候
如果用 Docker 不知道怎么搞呢?
zenika/alpine-chrome 可以用这个。
未梳理,只是讨论纪要。
Discuss 2019 06 19 Gorm Mysql Timestamp (`content/discuss/2019-06-19-gorm-mysql-timestamp.md`)
title: 2019-06-19 Go、Gorm 与 MySQL timestamp date: 2019-06-19T00:00:00+08:00
来源:『Go 夜读』微信群
Go、Gorm与MySQL中timestamp交互时遇到的问题
涉及到的方面
- MySQL中timestamp 默认值,explicit_defaults_for_timestamp属性设置
- Go中time.Time字段类型,time.Time零值
- Gorm中的处理方式
例如:
数据模型:
type A struct {
Id int
UserId int
VipExpireTime time.Time
MessageType string
ClickTabTime time.Time
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time"`
}对应字段:
CREATE TABLE `a` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`vip_expire_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'vip终止时间',
`message_type` varchar(50) NOT NULL DEFAULT '' COMMENT '消息的类型',
`click_tab_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '点击Tab的时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='a表';数据库初始化:
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type DBOrm struct {
Orm *gorm.DB
}
var DB DBOrm
const (
dbTestHost = "127.0.0.1"
dbTestUser = "root"
dbTestPwd = "123"
dbDevDB = "test1"
)
func InitGorm(user, password, addr, db string) {
var err error
DB.Orm, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
user, password, addr, db))
if err != nil {
panic(err)
}
DB.Orm.LogMode(true)
}
func InitDebug() {
InitGorm(dbTestUser, dbTestPwd, dbTestHost, dbDevDB)
}func timenull() {
InitDebug()
a := A{
UserId: 1,
// VipExpireTime: time.Time{},
}
err := DB.Orm.Table("a").Create(&a).Error
fmt.Println(err)
fmt.Printf("a: %+v", a)
}执行结果:
d:\mygo\src\ch\t>go test -v -run TestTimenull
=== RUN TestTimenull
?[35m(D:/mygo/src/ch/t/run.go:263)?[0m
?[33m[2019-06-18 16:41:16]?[0m ?[36;1m[1.01ms]?[0m INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,'0001-01-01 00:00:00','','0001-01-01 00:00:00')
?[36;31m[1 rows affected or returned ]?[0m
<nil>
a: {Id:3 UserId:1 VipExpireTime:0001-01-01 00:00:00 +0000 UTC MessageType: ClickTabTime:0001-01-01 00:00:00 +0000 UTC}--- PASS: TestTimenull (0.02s)
PASS
ok ch/t 0.266s发现这样gorm中操作是可以创建成功的。但是,如果粘贴insert语句到数据库中执行,是报错的。
INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,'0001-01-01 00:00:00','','0001-01-01 00:00:00')错误代码: 1292
Incorrect datetime value: '0001-01-01 00:00:00' for column 'vip_expire_time' at row 1造成这种时间的原因是什么呢?
大概是因为Go语言中time的初始值是第一年的一月一日这个设定。
Golang Time
想避免这种方式要怎么处理呢?
也带着问题问在夜读群中讨论。
「杨文:@我的名字叫浩仔丶Go 请教一个关于gorm create的问题,结构体user内部有一个time.Time字段a,对应数据库是timestamp类型,create是如果没有对a赋值,insert会报错,插入时间为0001-01-01了。修改方案想了两种,一个是给a赋time.Time{},另一种是将a改为指针的time,插入null。这两种哪种好呢,大家是怎么处理的呢?
@jinzhu gorm 作者」
「jinzhu:可以用 *time.Time ,或者类似 NullTime 这种类型」
「jinzhu:并且你的mysql应该是5.7之后的新版本吧,有个变量,允许 0001-01-01 这类数据。。。」
Gorm 作者提到的两种方式:
*time.Time(这貌似也是gorm issue里大部分的答案)NullTime
第一种方式
type A struct {
Id int
UserId int
VipExpireTime *time.Time
MessageType string
ClickTabTime *time.Time
}
a := A{
UserId: 1,
}
INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,NULL,'',NULL);这样gorm中操作是报错的。
Error Code: 1048. Column 'vip_expire_time' cannot be null另外此时又会引入新的问题,因为字段设置为指针类型,所以再取值时需要判断是否为null,否则会空指针。
在每一个用到VipExpireTime的地方,都需要判断
if VipExpireTime != nil { VipExpireTime.Format("2006-01-02 15:04:05") }这种代码让人头大!
再有Go Time的定义,也不建议用*time.Time。
Programs using times should typically store and pass them as values, not pointers. That is, time variables and struct fields should be of type time.Time, not *time.Time.
第二种方式:
// Scan implements the Scanner interface.
func (nt *NullTime) Scan(value interface{}) error {
nt.Time, nt.Valid = value.(time.Time)
return nil
}
// Value implements the driver Valuer interface.
func (nt NullTime) Value() (driver.Value, error) {
if !nt.Valid {
return nil, nil
}
return nt.Time, nil
}
func (nt *NullTime) MarshalJSON() ([]byte, error) {
if !nt.Valid {
return nil, nil
}
val := fmt.Sprintf("\"%s\"", nt.Time.Format(time.RFC3339))
return []byte(val), nil
}
type A struct {
Id int
UserId int
// VipExpireTime time.Time
// VipExpireTime *time.Time
VipExpireTime NullTime
MessageType string
// ClickTabTime time.Time
// ClickTabTime *time.Time
ClickTabTime NullTime
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time"`
}
type NullTime struct {
mysql.NullTime
}可以参照这篇文章做处理:How I handled possible null values from database rows in Golang?
然后我们说一下MySQL中explicit_defaults_for_timestamp属性,这与timestamp的默认值类型与表现形式有关。
注意:explicit_defaults_for_timestamp本身已被弃用,因为它的唯一目的是允许控制将来在MySQL版本中删除的已弃用的TIMESTAMP行为。当删除这些行为时,explicit_defaults_for_timestamp将没有任何用途,也将被删除。
查看explicit_defaults_for_timestamp当前的状态:
SHOW VARIABLES LIKE 'explicit_defaults_for_timestamp';
explicit_defaults_for_timestamp: OFF然后参考MySQL文档中给出的方式处理,Automatic Initialization and Updating for TIMESTAMP and DATETIME
因为timestamp会有 create_time、update_time这种字段,如果不赋值,gorm会按照零值处理,所以可以在字段后加tag
type A struct {
Id int
UserId int
VipExpireTime time.Time
MessageType string
ClickTabTime time.Time
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time on update current_time"`
}
对应数据库字段:
CREATE TABLE `a` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`vip_expire_time` timestamp DEFAULT 0 COMMENT 'vip终止时间',
`message_type` varchar(50) NOT NULL DEFAULT '' COMMENT '消息的类型',
`click_tab_time` timestamp DEFAULT 0 COMMENT '点击Tab的时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='a表';这样对于后续业务的时间判断,就可以利用 time.IsZero() 来判断。
看这个讨论Should I use the datetime or timestamp data type in MySQL?
当然直接把MySQL字段设置为datetime,也可以规避此类问题。但是对于使用datetime,还是timestamp?
个人还是倾向timestamp吧,因为随时区变化,空间效率更高。
扩展:
一、datetime 与 timestamp
datetime
范围最大,1001到9999年,时间格式为YYYYMMDDHHMMSS,与时区无关,使用8个字节存储。
如果没有指定 default,datetime 默认为 null。
Go中time.Time{}为零时,值为:0001-01-01 00:00:00 +0000 UTCtimestamp
保存了从1970年1月1日午夜以来的秒数,与UNIX时间戳相同。范围从1970年到2038年;使用4个字节存储;显示的值依赖于时区。
timestamp可以配置插入更新的行为,
如果没有指定 default,timestamp 默认为 0(即 1970-01-01 00:00:00)。
如果强行更新小于1970年的值,会报错:
Incorrect datetime value: '1969-12-01 00:00:00' for column 'ts' at row 1二、UTC/GMT/时间戳
1.UTC时间 与 GMT时间
我们可以认为格林威治时间就是时间协调时间(GMT=UTC),格林威治时间和UTC时间均用秒数来计算的。
2.UTC时间 与 本地时
UTC + 时区差 = 本地时间
时区差东为正,西为负。在此,把东八区时区差记为 +0800,
UTC + (+0800) = 本地(北京)时间 (1)
那么,UTC = 本地时间(北京时间))- 0800 (2)
3.UTC 与 Unix时间戳
在计算机中看到的UTC时间都是从(1970年01月01日 0:00:00)开始计算秒数的。所看到的UTC时间那就是从1970年这个时间点起到具体时间共有多少秒。 这个秒数就是Unix时间戳。
参考资料:
《高性能MySQL》
Automatic Initialization and Updating for TIMESTAMP and DATETIME
How I handled possible null values from database rows in Golang?
Discuss 2019 07 16 Assign And Range (`content/discuss/2019-07-16-assign-and-range.md`)
title: 2019-07-16 := 的由来和 range 遍历 date: 2019-07-16T00:00:00+08:00
来源:『Go 夜读』微信群
:= 的由来
0.23 BNF 和 EBNF,非终结符和终结符,开始符号及产生式
这都是辅助解释文法的概念。
BNF(Backus-Naur Form),即巴科斯范式,是一种程序设计语言描述工具,是由 John Backus 和 Peter Naur 引入的一种形式化符号,用来描述给定语言的语法,即描述语言的语言,故可以称之为元语言。
-> 定义为(也可用 := 和 ::= 表示)
The symbol is called "becomes" and was introduced with IAL (later called Algol 58) and Algol 60. It is the symbol for assigning a value to a variable. One reads x := y; as "x becomes y".
Using ":=" rather than "=" for assignment is mathematical fastidiousness; to such a viewpoint, "x = x + 1" is nonsensical. Other contemporary languages might have used a left arrow for assignment, but that was not common (as a single character) in many character sets.
Algol 68 further distinguished identification and assignment; INT the answer = 42; says that "the answer" is declared identically equal to 42 (i.e., is a constant value). In INT the answer := 42; "the answer" is declared as a variable and is initially assigned the value 42.
There are other assigning symbols, like +:=, pronounced plus-and-becomes; x +:= y adds y to the current value of x, storing the result in x.
(Spaces have no significance, so can be inserted "into" identifiers rather than having to mess with underscores)
range 删除元素
func main() {
// 删除切片中的某个元素
// 删除切片中指定元素
{
items := []int{1, 2, 4, 2, 3, 0}
deleteItem := 2
for i := 0; i < len(items); i++ {
if items[i] == deleteItem {
items = append(items[:i], items[i+1:]...)
i--
}
}
fmt.Println(items) // output: [1, 4, 3, 0]
}
// 删除找到的第一个元素
{
// 切片比较大的话,还是用普通的 for 循环比较好
items := []int{1, 2, 4, 2, 3, 0}
deleteItem := 2
for i, item := range items {
// 找到要删除的第一个元素,删除并退出循环
if item == deleteItem {
items = append(items[:i], items[i+1:]...)
break
}
}
fmt.Println(items) // output: [1, 4, 2, 3, 0]
}
}参考资料:
Discuss 2019 07 17 Global Variable Init (`content/discuss/2019-07-17-global-variable-init.md`)
title: 2019-07-17 go程序运行初始化顺序 date: 2019-07-17T00:00:00+08:00
来源:『Go夜读』微信群
时间:2019-07-17
1. go程序运行初始化顺序
go程序运行初始化顺序如图:
其实以前在看书时就看到过,不过当时并没有什么体会,通过下面这个问题代码,可以让读者对这个初始化顺序更有体会。
2. 问题代码描述
工程结构如图:
config/config.go:
package config
import (
"encoding/json"
"io/ioutil"
"log"
)
var (
// 单例
E_config *Config
)
type Config struct {
MongodbUri string `json:"mongodbUri"`
MongodbConnectTimeout int `json:"mongodbConnectTimeout"`
LogBatchSize int `json:"logBatchSize"`
LogCommitTimeout int `json:"logCommitTimeout"`
NodeAddress string `json:"nodeAddress"`
GenesisBlockMsg string `json:"genesisBlockMsg"`
BlockChainDatabaseEngine string `json:"blockChainDatabaseEngine"`
BlockChainDatabasePathTemplate string `json:"blockChainDatabasePathTemplate"`
}
// 读取配置
func InitConfig(configFile string) (err error) {
var (
content []byte
config Config
)
// 读取配置文件,得到[]byte内容
if content, err = ioutil.ReadFile(configFile); err != nil {
log.Println("读取配置失败")
return
}
// 反序列化
if err = json.Unmarshal(content, &config); err != nil {
return
}
// 赋值单例
E_config = &config
//log.Print(E_config)
return
}ledger/blockchain/chain.go:
package blockchain
import (
"log"
)
func InitChain() {
log.Println(dbEngine, dbPathTemp, nodeAddress)
}ledger/blockchain/config.go:
package blockchain
import "../../config"
var (
// json配置
dbPathTemp = config.E_config.BlockChainDatabasePathTemplate
dbEngine = config.E_config.BlockChainDatabaseEngine
nodeAddress = config.E_config.NodeAddress
)ledger/cli/cli.go:
package main
import (
"../../config"
"../blockchain"
"log"
)
func initConfig() {
const configFile = "config/enode.json"
if err := config.InitConfig(configFile); err != nil {
log.Panic(err)
}
}
func main() {
initConfig()
blockchain.InitChain()
}我的最初想法是在blockchain下新建config.go,将config.E_config的元素赋给config.go/dbEngine等,将这些从外部包引用的配置参数集中放置,达到提升代码可读性的目的,但是这似乎不行:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x58 pc=0x4cd34d]
goroutine 1 [running]:
_/E_/GO/XProjects/EChain-error/ledger/blockchain.init.ializers()
E:/GO/XProjects/EChain-error/ledger/blockchain/config.go:13 +0x2d
Process finished with exit code 2程序直接panic掉了,报错指向ledger/blockchain/config.go下dbEngine等变量的初始化,并指出产生了空指针调用。
但是看上去好像没有问题啊?main程序先执行配置初始化,得到E_config,再初始化Chain,Chain中使用E_config得到的元素值。
问题出在了变量初始化的顺序。根据go程序初始化顺序,一个程序在执行时,首先会去寻找import,然后会进行全局常量、全局变量然后是init()函数的初始化,最后才会执行到main()函数(也就是常说的程序入口)。
在我的代码中,dbEngine等变量就是blockchain中的包内全局变量,因此程序应该是先进行dbEngine等的初始化赋值,然而,此时尚未执行到main(),也就不会执行initConfig(),E_config也就仍然不存在。因此在dbEngine等元素赋值时我赋了一个根本不存在的值给到它们,这显然是无效的内存地址。
知道问题出在哪以后,就可以进行改正了,最简单的改正措施就是,将ledger/blockchain/config.go删掉,将dbEngine等变量初始化赋值语句放到InitChain中,使之由全局变量转为局部变量,这样,其初始化赋值操作就在InitConfig()之后了,也就不会产生空指针调用的错误。更正后代码及运行结果如下:
ledger/blockchain/chain.go:
package blockchain
import (
"../../config"
"log"
)
func InitChain() {
var (
// json配置
dbPathTemp = config.E_config.BlockChainDatabasePathTemplate
dbEngine = config.E_config.BlockChainDatabaseEngine
nodeAddress = config.E_config.NodeAddress
)
log.Println(dbEngine, dbPathTemp, nodeAddress)
}运行ledger/cli/cli.go/main()的结果:
2019/07/18 11:20:13 badger ./tmp/blocks/%s/blocks_%s 127.0.0.1:9797
Process finished with exit code 0这确实是从enode.json中读取的配置信息。说明现在没有问题了。
3. 起因及鸣谢
作为一个Go语言初学者及区块链初学者,在掌握了一些前置基础之后,我开始了EChain项目(一个基于区块链进行设备管理的物联网平台?)的开发(其实也是导师布置的任务)。在开发的过程中遇到了本文中提到的问题,非常感谢夜读群中Ryan等人热心的指导与帮助,在此谢过!另外希望EChain能够顺利完成!(来自菜鸟的祈祷~)
Discuss 2019 10 13 Go Module Looping Package (`content/discuss/2019-10-13-go-module-looping-package.md`)
title: go mod下出现looping trying to add package date: 2019-10-13T12:12:00+08:00
来源:『Go 夜读』微信群
时间:2019-10-13
问题
忽然之间出现looping trying to add package
意思就是循环引用某个类库,提示如下:
go: github.com/sirupsen/logrus imports
golang.org/x/sys/unix: looping trying to add package解决方法
删除go mod下的缓存,重新引用。
sudo rm -fr "$(go env GOPATH)/pkg/mod"sudo rm -fr "$(go env GOPATH)/pkg/mod”
go get github.com/rancher/[email protected]
After going through a screenful of dependencies, the program aborts with following error.
....
....
go: downloading github.com/rancher/k3s v1.14.1-k3s.1
go: finding github.com/rancher/k3s v0.3.0
go: downloading github.com/rancher/k3s v0.3.0
go: import "github.com/rancher/k3s": looping trying to add packageHarvest Harvest (`content/harvest/harvest.md`)
title: 你在 Go 夜读有哪些收获/感想呢? date: 2019-01-10T00:00:00+08:00
收获/感想
如果你觉得这个项目对你有帮助,请在这里写下哪些项目或者文章对你有帮助,具体帮了你哪些?你给后来人又有哪些建议?
收获1
分享人: @luojiego
更加清晰的人生方向
不再迷茫
在人生的第 30 个年头,终于明白人生的意义。19 年 9 月底,公司唯一一款产品因为一些原因被苹果下架。梦想破灭,我几乎要对游戏失去了兴趣,有想过放弃现在的环境,重新选择。但是,在西安这个比较小的环境里面,我也清楚的知道,没有其它的选择。虽然很失望,但是起码和老婆孩子在一起,每天下班回到家,儿子一直不停的叫着“爸爸”,也没有那么累了。
在这个比较关键的时刻,我终于开始听一年前媳妇推荐给我的某知识付费类产品。得益于这款软件中传达的许多理论,让我不再迷茫。9月24日被通知产品下架,10月8日我便从这个失意的过程中走出。因为在这段时间,吸收了许多的人生智慧,get到许多新技能。我从产品被下架的阴影中走出来了,同时也放下手里的烟,顺带明白了如何和媳妇孩子相处。我终于明白,活的不快乐只是因为内心不够充实,而物质的满足对于内心的满足无任何帮助,反而只会让欲望变得更强。
说来非常惭愧,从工作到现在近7年的时间,每天看似很努力。但是对比圈内的大佬,我简直就是个小学生。从来都是停留在应用层,偶尔也翻翻书,看一两行源码。在心态发生转变之后,我觉得应该更深入一些。于是在19年年底,有幸遇到**『Go 夜读』**,真是相见恨晚。
在 『Go 夜读』之前,我有次遇到个问题,在**大彬创建的 Go 语言充电站微信群里面,因为一个线上问题,得到煎鱼大佬的帮助。在该群里面,盛傲飞**每天尽 100% 的努力帮助大家解决 Go Modules 的问题,而我就像个啃老族,每天净能制造问题。
饶大的汇编角度看 Slice,让我对于 Slice 有了非常深刻的认识,我对 Slice 有疑惑的时候,第一反应就是将这个博客翻出来再看一次,真的写的非常棒。
**杨文**作为 Go 夜读的发起人,让我再不迷茫之后,知道接下来的目标是什么。我也要变强,也要为国内 Go 社区做出自己的贡献。
后面开始听『Go 夜读』的分享,**欧神**真的让我觉得我用两辈子也赶不上啦。CSP论文那一期,听到最后,我在B站的评论里面说的是“我和欧神的距离至少有78年“,而我平时英文文档几乎都不太愿意看。两周年纪念日,欧神说适当的阅读 Go 语言源码能够提升对语言本身更深层的认识;关于性能测试分享,欧神再次刷新我对全面的定义。
疫情期间,钟南山院士,张文宏等奋战在一线的战士,让我清楚的明白。我们的祖国正是被这一批神人保护的好好的。在我看来,各位大佬都是祖国需要的人,而我呢?我能做什么?我也要改变,我要将这门语言先往深的学习,跟『Go 夜读』的各位大佬好好学习,争取也加入到分享的队伍中来,从而实现自己的价值。
我的目标
- 遇到的疑惑通过研究源码寻找答案;
- 提升英文水平,能顺利读懂英文文档;
- 寻找分享的机会,早日加入『Go 夜读』,只是想分享知识给这个世界;
给新来小伙伴的建议
停止学习,在这个行业就离失业不远了,不能停止学习;
多写见解深刻的总结,多分享;
学好数学和英语,无论什么时候也不晚;
珍惜学校时间,知识和爱情最好兼顾,如果不能兼顾,选其一,用尽所有努力;
Interview Interview Algorithm (`content/interview/interview-algorithm.md`)
title: 算法题 date: 2018-07-26T00:00:00+08:00
1.如何在一个给定有序数组中找两个和为某个定值的数,要求时间复杂度为O(n),
比如给{1,2,4,5,8,11,15}和15?
func Lookup(meta []int32, target int32) {
left := 0
right := len(meta) - 1
for i := 0; i < len(meta); i++ {
if meta[left]+meta[right] > target {
right--
} else if meta[left]+meta[right] < target {
left++
} else {
fmt.Println(fmt.Sprintf("%d, %d", meta[left], meta[right]))
return
}
}
fmt.Println("未找到匹配数据")
}2.给定一个数组代表股票每天的价格,请问只能买卖一次的情况下,最大化利润是多少?日期不重叠的情况下,可以买卖多次呢?输入:{100,80,120,130,70,60,100,125},只能买一次:65(60买进,125卖出);可以买卖多次:115(80买进,130卖出;60买进,125卖出)?
func main() {
a := []int{100, 80, 120, 130, 70, 60, 100, 125}
// a := []int{68, 0, 1, 67}
var buyPrice, salePrice = 1<<31 - 1, -1 << 31
var buyDay, saleDay = -1, -1
type Op struct {
BuyDay, SaleDay int
BuyPrice, SalePrice int
Earnings int
}
var opList = []Op{}
// 遇到的第一个波谷买入,下一个波峰卖出,可以获取最大收益
for k, todayPrice := range a {
if buyDay == -1 {
// 寻找买入点
if todayPrice < buyPrice {
buyPrice = todayPrice
continue
}
// 买入
buyDay = k - 1
continue
}
// 寻找卖出点
if todayPrice > salePrice {
salePrice = todayPrice
if k < len(a)-1 {
continue
}
}
// 卖出
if k < len(a)-1 {
saleDay = k - 1
} else {
saleDay = k
}
opList = append(opList, Op{
BuyDay: buyDay,
SaleDay: saleDay,
BuyPrice: buyPrice,
SalePrice: salePrice,
Earnings: salePrice - buyPrice,
})
// 重复下一轮操作
buyPrice, salePrice = 1<<31-1, -1<<31
buyDay, saleDay = -1, -1
}
fmt.Printf("%+v", opList)
}3.给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
在这里只分享思路
首先,40亿个unsigned int的整数,如果放到内存,那就是大约16G的空间,
那么直接放到内存空间进行排序然后二分查找的方式是行不通的。方案一
在这里可以考虑使用bitmap,需要4*10^9bit内存, 大约500MB就可一把40
亿的数全部进行hash,时间复杂度是O(n),然后可以在O(1)的时间内进行判断此
数是否在40亿中;此过程在内存中完成。方案二
考虑在磁盘中操作。
因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;这里我们把40亿
个数中的每一个用32位的二进制来表示,假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类:
1.最高位为0
2.最高位为1
并将这两类分别写入到两个文件中;
再然后把这两个文件为又分成两类:
1.次最高位为0
2.次最高位为1
...
以此类推就可以找到了,而且时间复杂度为O(logn)Interview Interview Architecture (`content/interview/interview-architecture.md`)
title: 架构 date: 2018-07-26T00:00:00+08:00
1.Etcd满足了CAP原理中哪两个特性?
etcd是高可用的键值存储系统。满足CP原理
2.Etcd V2和V3版本的区别?
V2和V3接口不一致,存储不一样,数据互相隔离
A.V2是纯内存实现,并未实时将数据写入到磁盘;V3是内存索引(kvindex
: btree) + 后端数据库存储(boltdb: 单机的支持事务的kv存储)
B.V2过期时间只能设置到每个key上,如果多个key要保证生命周期一致则比
较困难;V3过期时间通过lease,可以给每个key设置相同的过期id
C.V2 Watch只能watch某一个key以及子节点(通过参数recursive),不能
进行多个watch;V3 watch机制支持watch某个固定的key,也支持watch一个范围
D.V2提供http接口;V3通过grpc提供rpc接口
3.当时选型etcd的考量是啥?和ZK有哪些区别?
相同点
1.应用场景类似: 配置管理,服务注册发现,选主,应用调度,分布式队列,分布式锁。
不同点
1.Etcd使用raft协议;Zk使用paxos协议,前者易于理解,方便工程实现。
2.Etcd相对来说部署方便;Zk的部署、维护、使用比较复杂,需要安装客户端。
3.Etcd提供http+json,grpc接口,跨平台语言; Zk则需要使用其客户端。
4.Etcd支持https访问; Zk在这方面缺失。
4.服务治理包含哪些?
服务注册和发现服务监控集群容错负载均衡
5.负载均衡分类?
1.DNS负载均衡(地理负载均衡)
2.硬件负载均衡(F5和A10,能支撑200万-800万/秒并发,价格昂贵)
3.软件负载均衡(Nginx和Lvs,nginx支撑5万/秒,LVS支撑80万/秒,价格便宜,
扩展方便)
并发访问量大于1000万时可以考虑配合使用
6.Nginx和Lvs的区别?
1.Nginx 大多工作在第7层,网络的依赖比较小,可以对HTTP应用实施分流策略,比如域名、结构等(也可以工作在第4层网络层,例如对mysql做反向代理,做授权和负载均衡等);Lvs工作在第4层,比较依赖网络环境,可以对几乎所有应用进行负载均衡,包括Web、数据库等
2.Nginx负载能力强,因为其工作方式逻辑非常简单,仅进行请求分发,没有流量;Lvs负载能力较差,受限于机器的I/O配置(处理流量)
3.Nginx安装,配置及测试相对简单;Lvs的安装、配置及测试所花的时间比较长
7. 你们是怎么解决微服务之间调用优化与问题排查的?
关键词: 链路监控, Google Dapper 论文
搭建以链路追踪技术为核心的监控系统,通过收集、存储、分析、分布式系统中的调用事件数据,协助开发运营人员进行故障诊断、容量预估、性能瓶颈定位, 调用链路梳理以及优化服务依赖
实施时需要注意埋点导致的性能问题, 使用异步队列的方式和调整采样率来减少对服务本身造成的性能影响
常用开源方案: zipkin, jaeger, cat,Skywalking.....
Jaeger 是 Uber 开源的分布式追踪方案, 已加入 CNCF, 基于 Golang 开发, 部署方便, 支持 Docker, 唯一不足是只支持 Cassandra 和 Elasticsearch...
更多可自行搜索, 可参考:
8. 介绍下 API 网关的作用和应用架构?
API网关,它负责在上层抽象出各业务系统需要的通用功能,例如:鉴权、限流、ACL、降级等。
随着微服务的流行,API网关已经成为一个微服务架构中的标配组件。查看资料:
Interview Interview Data Structure (`content/interview/interview-data-structure.md`)
title: 数据结构 date: 2018-07-26T00:00:00+08:00
1.什么是跳跃表?
跳跃表是基于有序链表的一种扩展
2. 介绍下 RESTFull API 方式下, 怎么做到快速路由?
一般使用前缀树/字典树, 来提高查找速度.
开源的路由模块里, httprouter 是比较快的, 参考: https://github.com/julienschmidt/httprouter
他使用了一种改进版的前缀树算法. 这个树的应用非常广泛, 除了做路由, 还有 linux 内核里使用, 在数据库里也有用到.
参考文章:
Interview Interview Database (`content/interview/interview-database.md`)
title: 数据库 date: 2018-07-26T00:00:00+08:00
1.Mysql事物的隔离级别?
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(read-uncommitted) | 是 | 是 | 是 |
| 读已提交(read-committed) | 否 | 是 | 是 |
| 可重复读(repeatable-read) | 否 | 否 | 是 |
| 串行化(serializable) | 否 | 否 | 否 |
2.Innodb和Myisam的区别?
Innodb支持事务,而Myisam不支持事务
Innodb支持行级锁,而Myisam支持表级锁
Innodb支持外键,而Myisam不支持
Innodb不支持全文索引,而Myisam支持
Innodb是索引组织表, Myisam是堆表
3.Mysql慢响应默认时间?
10s
4.Explain的含义?
explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助
选择更好的索引和写出更优化的查询语句。
5.Profile的意义以及使用场景?
Profile用来分析SQL性能的消耗分布情况。当用explain无法解决慢SQL的时
候,需要用profile来对SQL进行更细致的分析,找出SQL所花的时间大部分消耗在
哪个部分,确认SQL的性能瓶颈。
6.Redis的过期失效机制?
scan扫描+给每个key存储过期时间戳
7.Redis持久化方案aof的默认fsync时间是多长?
1s
8.Redis持久化方案rdb和aof的区别?
9.Redis怎么查看延迟数据?(非业务操作)
可以用redis-cli工具加--latency参数可以查看延迟时间
redis-cli --latency -h 127.0.0.1 -p 6379使用slowlog查出引发延迟的慢命令
slowlog get
10.Redis的集群怎么搭建?
11.简单介绍下什么是缓存击穿, 缓存穿透, 缓存雪崩? 能否介绍些应对办法?
12.关系型数据库 MySQL/PostgreSQL的索引类型? 其他数据库优化方法?
13.介绍下数据库分库分表以及读写分离?
分库分表主要解决写的问题, 读写分离主要解决读的问题.
分库分表的策略有很多种: 平均分配, 按权重分配, 按业务分配, 一致性 hash.....
读写分离的原理大致是一台主、多台从。主提供写操作,从提供读操作.
方案可以根据以下几个因素来综合考虑:1.数据实时性要求?
2.查询复杂度是否比较高?
3.读和写的比例即侧重点是哪一个?方案有很多, 大家可以自行搜索, 学习总结.
这题源自微博的平台技术专家一条微博: https://m.weibo.cn/status/4265027340366901
Interview Interview Design (`content/interview/interview-design.md`)
title: 设计题 date: 2018-07-26T00:00:00+08:00
1.要设计一个秒杀系统要注意什么?
前端秒杀页面
页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态
元素。通过CDN来抗峰值。禁止重复提交:用户提交之后按钮置灰,禁止重复提交。用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流。
2.要设计一个类似微信红包架构系统要注意什么?
南北分区快慢分离Hash负载均衡Cache屏蔽DB双维度分库表
Interview Interview Golang Language (`content/interview/interview-golang-language.md`)
title: Golang语言 date: 2018-07-26T00:00:00+08:00
1.select是随机的还是顺序的?
select会
随机选择一个可用通道做收发操作
2.Go语言局部变量分配在栈还是堆?
Go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做
逃逸分析,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
3.简述一下你对Go垃圾回收机制的理解?
v1.1 STW
v1.3 Mark STW, Sweep 并行
v1.5 三色标记法
v1.8 hybrid write barrier(混合写屏障:优化STW)
4.简述一下golang的协程调度原理?
M(machine): 代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。P(processor): 表示逻辑processor,是线程M的执行的上下文。G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
5.介绍下 golang 的 runtime 机制?
Runtime 负责管理任务调度,垃圾收集及运行环境。同时,Go提供了一些高级的功能,如goroutine, channel, 以及Garbage collection。这些高级功能需要一个runtime的支持. runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个user space的独立的可执行文件。
从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。
Go runtime的一个重要的组成部分是goroutine scheduler。他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。
6.如何获取 go 程序运行时的协程数量, gc 时间, 对象数, 堆栈信息?
调用接口 runtime.ReadMemStats 可以获取以上所有信息, 注意: 调用此接口会触发 STW(Stop The World)
参考: https://golang.org/pkg/runtime/#ReadMemStats
如果需要打入到日志系统, 可以使用 go 封装好的包, 输出 json 格式. 参考:
更深入的用法就是将得到的运行时数据导入到 ES 内部, 然后使用 Kibana 做 golang 的运行时监控, 可以实时获取到运行的信息(堆栈, 对象数, gc 时间, goroutine, 总内存使用等等), 具体信息可以看 ReadMemStats 的那个结构体
效果大致如下:
7.介绍下你平时都是怎么调试 golang 的 bug 以及性能问题的?
- panic 调用栈
- pprof
- 火焰图(配合压测)
- 使用go run -race 或者 go build -race 来进行竞争检测
- 查看系统 磁盘IO/网络IO/内存占用/CPU 占用(配合压测)
8.简单介绍下 golang 中 make 和 new 的区别
new(T) 是为一个 T 类型的新值分配空间, 并将此空间初始化为 T 的零值, 并返回这块内存空间的地址, 也就是 T 类型的指针 *T, 该指针指向 T 类型值占用的那块内存.
make(T) 返回的是初始化之后的 T, 且只能用于 slice, map, channel 三种类型. make(T, args) 返回初始化之后 T 类型的值, 且此新值并不是 T 类型的零值, 也不是 T 类型的指针 *T, 而是 T 类型值经过初始化之后的引用.
参考:
Interview Interview Network (`content/interview/interview-network.md`)
title: 网络 date: 2018-07-26T00:00:00+08:00
1.tcp三次握手和四次挥手流程示意图?在黑板上画出
2.客户端在建立异常中发现很多connect reset by peer,你觉得问题出在哪?
三次握手维护的半连接队列或者全连接队列溢出导致
3.https建立连接的过程?
1.客户端发送请求到服务器端
2.服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在
3.客户端验证证书和公开密钥的有效性,如果有效,则生成对称密钥并使用公开密钥加密发送到服务器端
4.服务器端使用私有密钥解密数据,并使用收到的对称密钥加密数据,发送到客户端
5.客户端使用对称密钥解密数据
6.SSL加密建立………
4.tcp和udp的区别?
1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前
不需要建立连接
2.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,
不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3.TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向
报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的
交互通信
5.TCP首部开销20字节;UDP的首部开销小,只有8个字节
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
5.http,tcp,ip分别处于OSI哪一层?
传输层协议:TCP、UDP、SCTP网络层协议:IP、ARP、RARP、ICMP、IGMP、OSPF应用层协议:http,FTP、SMTP、RIP、DNS
Interview Interview Os (`content/interview/interview-os.md`)
title: 操作系统 date: 2018-07-26T00:00:00+08:00
1.Select,Poll,Epoll的区别?
select,poll,epoll都是IO多路复用的机制,具体区别请查阅资料
2.什么叫虚拟内存?
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的
可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内
存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
3.什么叫桥接?
桥接是指依据OSI网络模型的链路层的地址,对网络数据包进行转发的过程,工作
在OSI的第二层;一般的交换机,网桥就有桥接作用。
4.Linux什么命令可以查看cpu和内存?怎么查看每个核的cpu呢?
top命令
在top查看界面按数字1即可查看每个核的数据
5.给一个PID=100你觉得它是后台程序还是前台程序?
进程号0-299保留给daemon进程
6.怎么查看一个端口的TCP连接情况?
netstat
7.Docker的网络模式有哪几种?
bridge网络
host网络
none网络
container模式
8.介绍一下Tcpdump?
tcpdump网络数据包截获分析工具。支持针对网络层、协议、主机、网络或端口
的过滤。并提供and、or、not等逻辑语句帮助去除无用的信息。
9. 什么叫大端和小端?
说明
1.Little-Endian(小端)就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端
2.Big-Endian(大端)就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。使用场景
一般
操作系统都是小端的,而通信协议是大端的
10. 介绍下 docker 底层原理
11.介绍些僵尸进程和孤儿进程的区别, 怎么产生的, 怎么避免?
12.CPU 使用率和 CPU 负载有什么区别?
Interview Interview Pen (`content/interview/interview-pen.md`)
title: 笔试题 date: 2018-07-26T00:00:00+08:00
1.Golang笔试题解析
Night 1 2018 03 21 Goutil (`content/night/1-2018-03-21-goutil.md`)
title: 第 1 期 2018-03-21 线下分享内容 date: 2018-03-21T11:49:10+08:00
参与人数: 3 人
cannot take address of temporary variables
延伸阅读
- https://stackoverflow.com/questions/10535743/address-of-a-temporary-in-go
- https://stackoverflow.com/questions/40926479/take-the-address-of-a-character-in-string
- https://golang.org/ref/spec#Address_operators
neochain
[teleport](https://github.com/henrylee2cn/teleport)
Teleport 是一个通用、高效、灵活的Socket框架。
可用于 Peer-Peer 对等通信、RPC、长连接网关、微服务、推送服务,游戏服务等领域。
[goutil](https://github.com/henrylee2cn/goutil)
通用的 Go 开发工具包
- Calendar
- CoarseTime
- ...
Night 1 2019 03 14 Daily Reading (`content/night/1-2019-03-14-daily-reading.md`)
title: 第 1 期每日阅读特训营 date: 2019-03-14T09:10:00+08:00
阅读清单
| 标题 | 阅读者 |
|---|---|
| 漫话:如何给女朋友解释为什么有些网站域名不以 www 开头 | mai |
| LeetCode 的刷题利器(伪装到老板都无法 diss 你没有工作) | mai |
| 深入 GO 语言文本类型 | ch |
| k8s cpu 资源限制 k8s 内存资源限制 | Jason |
| Golang 之轻松化解 defer 的温柔陷阱 关于 go 语言中的延迟执行函数 | Littlesqx |
“漫话”的阅读笔记
- 域名的一个重要功能——为数字化的互联网资源提供易于记忆的名称。
- 域名具有唯一性。
- www,其实是 World Wide Web 的缩写,中文翻译为万维网
- 互联网并不等同万维网(WWW),万维网只是一个基于超文本相互链接而成的全球性系统,且是互联网所能提供的服务其中之一。
- 为了区分互联网中的各种应用,就有了不同的子域名,比如互联网就以 www 作为子域名,文件传输以 ftp 作为子域名,电子邮件以 mail 作为子域名。
- 正是因为万维网是互联网中最重要的一部分,很多域名的最主要用途也是搭建 web 网站,所以,会有很多公司直接忽略 www。
- 通用顶级域(英语:Generic top-level domain,缩写为 gTLD),是互联网名称与数字地址分配机构(IANA)管理的顶级域(TLD)之一。该机构专门负责互联网的域名系统。
- 域名支持中文,并且域名中也已经支持颜文字了。“👀.我爱你”
“Go文本类型”的阅读笔记
- 为什么字符串直接取下标,s[0] 类型是 byte,range 时 类型为 rune?
- k,v := range string,k 是 UTF-8 编码字节的下标,v 是 Unicode。For statements with range clause
- 扩展 fmt 格式化
k8s 资源限制
- pod 调度是按 container 中 request 资源总和调度的;
- limit 帮助 kubelet 约束本节点上容器资源使用最大份额;
- 针对 内存 这种不可压缩资源,超额将导致 container 中程序 OOM 退出;而 CPU 只是影响了程序的等待时长;
- 内存资源:cgroup 中
memory.limit_in_bytes(最大内存 limit 份额)、memory.soft_limit_in_bytes(request 内存份额) - CPU 资源,分两部分:
- request cpu 份额: cgroup 中
cpu.shares,其中记录的数值是 CPU 分片数,一个 CPU 分为 1024 片(k8s 分成 1000 片),request.memory: 500m 表示申请 500/1000 个 cpu; 它保证程序能接收到申请的份数 CPU 片,但是如果程序没完全使用,其他程序是可以占用的; - limit cpu 份额:
cpu.cfs_period_us表示一个 CPU 执行时间周期的时间,通常为 100ms(带宽控制系统定义了一个通常是 1/10 秒的周期);cpu.cfs_quota_us表示一个周期内分配的 CPU 时间; cpu.cfs_period_us/cpu.cfs_quota_us 合起来表示 limit 的 CPU 份额;
- request cpu 份额: cgroup 中
- 查看 cgroup 命令:
1. 找到容器中程序在宿主机上进程 ID
$ cat /proc/${PID}/cgroup
...
8:cpuacct,cpu:/kubepods/burstable/podxxx/dockerid
7:memory:/kubepods/burstable/podxxx/dockerid
...
2. ls /sys/fs/cgroup/cpuacct,cpu/kubepods/burstable/podxxx/dockerid/
3. ls /sys/fs/cgroup/memory/kubepods/burstable/podxxx/dockerid“defer” 笔记
- 每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当在当前函数执行完毕后(包括通过 return 正常结束或者panic 导致的异常结束),defer 函数按照定义的逆序执行;如果 defer 执行的函数为 nil, 那么会在最终调用函数的产生 panic。
- Golang 内置的带返回值的函数无法进行延迟调用(调用的结果不可以抛弃,copy 和 recover 例外),可通过放入到匿名函数中再 defer。
- defer 常用场景:控制资源(文件、数据库、锁等)的申请和释放,使代码简洁;配合 recover 实现异常恢复。
- defer 副作用(坑):延迟执行的机制损耗性能;延迟意味着资源占用,需要时刻警惕尽可能早地释放。
观看视频
{{< youtube id="" >}}
Night 10 2018 06 28 Net Http Part4 (`content/night/10-2018-06-28-net-http-part4.md`)
title: 第 10 期 2018-06-28 线下活动 date: 2018-06-28T11:49:10+08:00
参与人数: 10 人
Go 标准包阅读
- Go版本:
go 1.10.2
net包
- http/server.go
- http/request.go
- textproto/reader.go
读取位置
- textproto/reader.go(
140行)
问题
1.各个系统的回车换行符区别
- 注意:
10.13及其以上是macOS系统
2.URI,URL和URN的区别
3.HTTP CONNECT方法介绍
会议讨论小结
可以建立一个代理服务器到目标服务器的透明通道(tcp连接通道),中间完全不会对数据做任何处理,直接转发(支持https,一种翻墙的手段,专线独享)4.peek读取字节内部实现
- 这里先peek获取流数据(注意:
这里没有对Peek的错误进行处理,而是根据是否Buffered读取到数据来判断错误) - 为什么没有对Peek的错误进行处理呢?
主要是因Peek失败了也有可能不会返回错误
golang读取字节表现形式是阻塞式的,但其实底层是用了非阻塞式的NIO,如果没有读取到数据会定时轮询读取5.http header尾部的符号什么情况下会存在\n\n的情况?(
待解决,欢迎在下面评论)
看源码发现hearder结尾会存在\r\n\r\n和\n\n两种字符情况
网络上查资料发现只会存在\r\n\r\n
观看视频
{{< youtube id="xodlVBWxTYM" >}}
- TODO
相关链接
Night 104 2020 09 13 Hashicorp Raft (`content/night/104-2020-09-13-hashicorp-raft.md`)
desc: Go 夜读之 通过 hashicorp/raft 库手把手调试 raft 算法 title: 第 104 期通过 hashicorp/raft 库手把手调试 raft 算法-文字版 date: 2020-09-13T10:30:00+08:00 author: 黄威
概览(00:00~01:11)
欢迎收看今天的分享,我是黄威,是一名趣头条的后端工程师.今天跟大家分享的主要是关于 raft 算法的一个工程实践,就是它用 go 语言是怎样实现的,跟大家分享一下.
首先看一下今天大概的内容,主要是 raft 协议的简介,还有选举过程和日志复制过程的简单介绍,然后是官方的动画的演示,再接着就是源码的讲解,就是hashicorp/raft这个库源码的讲解,然后剩下的就是场景的一些调试,到时候我会修改一些 hashicorp/raft 源码,修改一些日志,到时候方便大家 Debug.最后就是 QA,大概今天是这些内容。
Raft 简介(01:12~03:08)
我们先从第一部分开始吧。首先,大家听这个分享应该有一点 raft 的算法的一些了解,所以这里不会有太详细的介绍。Raft 是一种为了管理复制日志复制一致性协议,其实就是这个论文的第一句话,它里面其实有一个复制状态机的概念,当然他这个不是他提出来的,这是分布式系统里面一个基础的改进,它的意思是说你的系统或者其他的程序,如果初始状态一致,然后接受一些改变命令的状态也是一致,最后产生的结果状态也是一致的,这是复制状态机的概念,raft 协议里面有所体现。第三个就是 raft 的论文的中英文地址,这里都有,大家可以看一下,也可以对照着看中英文。需要注意的是论文其实有两个,一个是简介版的,一个是完整版的,完整版的就是 raft 发明者写的,他的博士论文大概有 200 多页,都是英文的,如果大家有余力可以去看一下。这里主要就是通过简单的论文,来跟大家介绍,它基本上涵盖了 Raft 核心,这个论文也能够了解 Raft 协议的核心的内容。
Raft 选举的过程(03:09~05:56)
先来跟大家,简单介绍一下选举的这个过程吧:
首先是以单个节点的视角,就是说,你的一个节点,应该都是这三个角色(follower,canidate,leader)中的一个,并且是在这三个角色中变换。你的集群(节点)起来时应该是一个 follower 的一个状态,然后经过一个 timeout 时间变成 candidate (候选者),然后变成 candidate 以后,你会向其他节点发送投票的请求,如果说你收到一个 majority 或者 quorum 数量的同意投票的请求,那么你就会提升为 leader,这个 majority 或者 quorum 其实就是你节点集群中,大多数节点的数量,比如说你三个结点就是两个节点,四个的话就是三个,五个的话就是就是也是三个,这样子就是取其中大多数节点,如果说同意你的投票请求,那么你就会提升为 leader,然后 leader 会因为一些情况,比如分区啊,或者说联系不上 follower 等,还有一其他的情况,会变成 follower,这个是大概相当于你的那个一个节点,你可能会在这些状态里面来回的切换.
然后对于你整个集群的话,你的集群集的状态就是要么在选举,要么就是在选举完成以后正常提供服务的一个状态,蓝颜色的就是在选举,绿色就是选举完成以后,正常提供服务的状态,比如说你集群刚起来肯定在一个 term1, 选举的一个过程;选举出来以后,它就是正常服务的状态,然后到了 term2,就是可能因为某些原因,然后重新选举了,然后又选举出来,然后又重新选举 term3,term3 这里,可能这个任期没有选举出,经历了一个 timeout 的时间,我们可以看上一张图,当前的 candidate 候选者,在自己当前轮没有收到大多数节点的统一的请求,也没有收到其他 leader 对自己的一些心跳的通知,那么它会进入进入下一轮的选举,就是下面这张图的 term4,然后通过选举完成以后,然后又进入一个正常的一个状态,这个大概就是选举的一个过程。
Raft 日志复制的流程(05:56~09:18)
接下来是关于日志复制的一个流程,也跟大家简单介绍一下,首先讲一下日志的格式。
日志格式
日志格式:term + index + cmd + type
日志的格式的话是有任期,索引还有数据,cmd,还有就是 type.这里可以给大家直接看下数据结构:
// Log entries are replicated to all members of the Raft cluster
// and form the heart of the replicated state machine.
type Log struct {
// Index holds the index of the log entry.
Index uint64
// Term holds the election term of the log entry.
Term uint64
// Type holds the type of the log entry.
Type LogType
// Data holds the log entry's type-specific data.
Data []byte
// Extensions holds an opaque byte slice of information for middleware. It
// is up to the client of the library to properly modify this as it adds
// layers and remove those layers when appropriate. This value is a part of
// the log, so very large values could cause timing issues.
//
// N.B. It is _up to the client_ to handle upgrade paths. For instance if
// using this with go-raftchunking, the client should ensure that all Raft
// peers are using a version that can handle that extension before ever
// actually triggering chunking behavior. It is sometimes sufficient to
// ensure that non-leaders are upgraded first, then the current leader is
// upgraded, but a leader changeover during this process could lead to
// trouble, so gating extension behavior via some flag in the client
// program is also a good idea.
Extensions []byte
}你看它的定义的数据结构,其实也是一样,有 index,term,type,data,下面是扩展信息不用管。定义的这些字段上保存 log.
这张图上面一排是索引,索引的位置第一个位置,第二个位置,然后到一直到 100 多,下面的框里面这个数字,中间的这个数字是代表任期,比如这个第一个任期,第二个任期,第三个任期, X 等于 3,相当于是数据,就是这个论文里面,它其实是以 KV 服务来举例的,那他保存的肯定是一些 kv 的一些命令。这个就是 X 等于 3,y 等于 1, X 等于 2,第 2 个任期提交,以 4 这个位置提交的 x 等于 2,在位置 8 这个位置提交的 X=4...
这大概是一个日志的格式,并且就是说,如果说日志复制到了大多数的节点上,那么他就会提交,所以这里面这张图的话,他 1 到 4 这边 1 到 7 这个位置就是,被提交了的。
请求处理整体流程
然后就是提交整体的处理流程。比如你一个客户端发送一个发送请求,发送到 leader 的服务器,然后会通过你编写的一个 raft 的模块,跟底层的 raft 的库进行交互,你先把日志存起来,然后在并行的发送给其他的节点。如果你收到大多数的成功的响应,那么你肯定就可以提交这个日志。第三步会写到状态机,然后第四步就返回给客户端,就是大于大致的一个流程.
请求处理详细流程(重点)
下面是一个详细的流程,到时候后面我们也会根据这个图来 Debug
我会把这里面每一步执行的日志都会打印出来,并且会带着大家分析一下,就是打印日志每一步的上下文,这样的话大家就可以更实际地理解 raft 的实现,会有更直观的一个认识,这个图我后面再反过来跟大家讲一下。
Raft 协议动画演示(09:19~12:40)
选举和日志复制的流程的话,跟大家简单的过了一下。
这里有有两个官方的动画的演示(link1,link2),第一个图就不跟大家演示了,就直接看第二个吧。它是模拟 raft 集群的实时的状态,比如说你的集群刚起来,现在都是橘黄色,都是一个 follower 的状态。然后让他继续运行,你看它这个圈就是外层的灰色的线,它是代表一个选举超时,就是 election timeout,所以说如果 follower 没有收到 leader 的请求的话,那么他就会经历一个 timeout 自动的提升为 candidate,然后就会发送投票请求,如果说收到了大多数节点的同意的响应,那么它就会成为 leader。哪个先到 candidate 就会发送投票请求,肯定只会有一个收到大多数的节点的投票的请求,肯定不会有两个的。现在这个 S5 这个服务器变成了 leader,然后他就一直保持心跳,发送心跳的信息。我们先来给他发送一个请求,他会先写到自己的日志,然后我们再让他执行,会被发送到其他的的节点。其他的节点复制完成以后,他就可以返回给客户端请求处理成功。应该是提交信息以后。S5 第二个任期这里,现在就变成实线了,那说明他提交了,接着,它就会发送提交的信息给其它的节点,那其他节点也提交了.
我们再来看一下这个过程:
你自己复制给,其他所有的也复制,然后就自己提交,然后在其他的也提交;如果说我们把它宕掉,会发生什么呢?然后没有心跳,其他收不到,然后就会自动触发选举的流程。然后就会选举出一个新的 leader,现在是 S3,然后我们给他一个请求,也是一样的,但是现在 s2 收不到日志的,日志是落后的,但是没关系,我们把它恢复起来,然后他就会很快就会赶上来。
这个大概就是一个 raft 的选举和复制日志的一个流程动画演示。大家有兴趣啊,到时候也可以自己去看一下。
完整讲解 hashicorp/raft(12:40~28:18)
接下来是,跟大家介绍今天的重点之一,就是圆满的讲解下 hashicorp/raft 这个库。先给大家简单的讲解一下我写的kv 服务,它其实就是一个简单的 kv 服务,然后通过 HTTP 的地址来对外提供服务的,我们先看一下这个我这个服务的代码吧,入口在 main 文件里,这个服务呢,我写的应该是很简单的,总共其实也只有三个文件,一个 main.go,一个 myraft 的,一个就是状态机。
初始化
我们先从这个 main 函数里面开始吧,先看一下这个服务是怎样启动起来的,然后我们在进入 raft 的源码去看一下。
func main() {
flag.Parse()
// 初始化配置
if httpAddr == "" || raftAddr == "" || raftId == "" || raftCluster == "" {
fmt.Println("config error")
os.Exit(1)
return
}
raftDir := "node/raft_" + raftId
os.MkdirAll(raftDir, 0700)
// 初始化raft
myRaft, fm, err := myraft.NewMyRaft(raftAddr, raftId, raftDir)
if err != nil {
fmt.Println("NewMyRaft error ", err)
os.Exit(1)
return
}
// 启动raft
myraft.Bootstrap(myRaft, raftId, raftAddr, raftCluster)
// 监听leader变化 (其实这个的话在实际的工程工作实际的开发中,其实应该是不常用的这里面。为了演示简单,然后用了一下)
go func() {
for leader := range myRaft.LeaderCh() {
isLeader = leader
}
}()
// 启动http server
httpServer := HttpServer{
ctx: myRaft,
fsm: fm,
}
http.HandleFunc("/set", httpServer.Set)
http.HandleFunc("/get", httpServer.Get)
http.ListenAndServe(httpAddr, nil)
// 关闭raft
shutdownFuture := myRaft.Shutdown()
if err := shutdownFuture.Error(); err != nil {
fmt.Printf("shutdown raft error:%v \n", err)
}
// 退出http server
fmt.Println("shutdown kv http server")
}初始化一些,配置也是从命令行参数读取,然后第二个就是初始化 raft,里面就是一些 raft 初始化的信息。这个就是启动 raft,然后这个就是进行一点点变化,其实这个的话在实际的工程工作实际的开发中,其实应该是不常用的这里面。为了演示简单,然后用了一下。接着就是其中一个 httpserv,他到这里会阻塞。这里是两个 handler,一个是 set,一个是 get,set 是直接执行命令的一个入口,get 就是简单的一个 get.下面就是关闭了。正常的话应该是到这里阻塞住,如果说我也退出了,他会继续往下.
我们现在主要就是看这两部分(myraft.NewMyRaft 和 myraft.Bootstrap):
func NewMyRaft(raftAddr, raftId, raftDir string) (*raft.Raft, *fsm.Fsm, error) {
config := raft.DefaultConfig()
config.LocalID = raft.ServerID(raftId)
// config.HeartbeatTimeout = 1000 * time.Millisecond
// config.ElectionTimeout = 1000 * time.Millisecond
// config.CommitTimeout = 1000 * time.Millisecond
addr, err := net.ResolveTCPAddr("tcp", raftAddr)
if err != nil {
return nil, nil, err
}
transport, err := raft.NewTCPTransport(raftAddr, addr, 2, 5*time.Second, os.Stderr)
if err != nil {
return nil, nil, err
}
snapshots, err := raft.NewFileSnapshotStore(raftDir, 2, os.Stderr)
if err != nil {
return nil, nil, err
}
logStore, err := raftboltdb.NewBoltStore(filepath.Join(raftDir, "raft-log.db"))
if err != nil {
return nil, nil, err
}
stableStore, err := raftboltdb.NewBoltStore(filepath.Join(raftDir, "raft-stable.db"))
if err != nil {
return nil, nil, err
}
fm := new(fsm.Fsm)
fm.Data = make(map[string]string)
rf, err := raft.NewRaft(config, fm, logStore, stableStore, snapshots, transport)
if err != nil {
return nil, nil, err
}
return rf, fm, nil
}进去一句的话,是先初始化 raft 的配置,如果你不是特别清楚的话,其实就用他原来的就好,特别是这些 timeout,为了调试方便,我会改一
然后是初始化,比如说这个(ResolveTCPAddr)是初始化一个 http 地址,这个(NewTCPTransport)是一个 transport,用来发送发送 RPC 一个结构体 ,这个(NewFileSnapshotStore)是快照的,那这个(logStore)就不多说了,这个日志我到时候会存到这个存储,这里 raft 库用的是 boltdb 的,你也可以用一些成熟的库,但是你要实现他的接口。这个(stableStore)是存储一些 raft 的状态,然后这个(fm)是相当于是你自己的一个应用层的状态机。后面再反过来跟大家讲一下这个状态机是怎么用的,然后这个 NewRaft 的就正式进入到 hashicorp/raft 这个库的内部里面。我们继续跟踪下去:
/* Detailed source-code truncated for AI context efficiency. */它的启动流程的话就是先校验一下配置,然后 log 的初始化,然后是获取当前的任期,这个任期的话必须得持久化。大家可以看一下这个论文里面这张图。
这张图的话应该说是描述的就是整个 raft 的算法,就是你的数据结构要怎么样设置?然后有那些 RPC 的方法,就是投票和复制日志的 RPC 方法,你应该实现什么样的逻辑啊,在这里都有。这个(state)呢,比如说你要持久化的话,他肯定会持久化当前的任期,还有投票,这里就会取出来。这个(logs.LastIndex())是获取最新日志的索引,接着就是构造 raft 的实例。这个(r.setState(Follower))是设置角色,就是你的集群起来,肯定都是以 follower 的角色起来。这里(setCurrentTerm)设置当前任期,这个(restoreSnapshot)是从快照里面恢复一些数据。这个(processConfigurationLogEntry)是对日志里面一些配置类型的 log 进行处理。然后这里有个 SetHeartbeatHandler的 handler,这里是这样的,正常的话你的 heartbeat 应该是跟 AppendEntries RPC 是同一个 handler,但是一个问题就是说,如果说你的前面一个 AppendEntry 执行很久,然后后面 heartbeat 如果发送请求的话,可能会阻塞,所以就没有达到 heartbeat 或者 ping 的这个效果,就会有问题,所以它是单独初始化了一个 handler,然后,但是呢,他的逻辑是跟这个(AppendEntry)一样的,他们用的都是同一个方法,这个大家可以注意一下.下面就是这,里面就是 raft 算法的核心实现了。
raft 的监听事件
// run is a long running goroutine that runs the Raft FSM.
func (r *Raft) run() {
for {
// Check if we are doing a shutdown
select {
case <-r.shutdownCh:
// Clear the leader to prevent forwarding
r.setLeader("")
return
default:
}
// Enter into a sub-FSM
switch r.getState() {
case Follower:
r.runFollower()
case Candidate:
r.runCandidate()
case Leader:
r.runLeader()
}
}
}我们接下来进入就是讲 raft 的监听事件,到时候如果大家看这个库的话,肯定也是以这里为入口,这里就开始 raft 的逻辑了,这是 follower 的逻辑和 candidate 的逻辑,还有 leader 的逻辑,其实比较清晰了。
我们先看 runFollower 吧,其中一个都会监听一些 channel,然后执行执行一些逻辑,
/* Detailed source-code truncated for AI context efficiency. */对于 follower 来说,其实最重要的两个其实就是 AppendEntries RPC 和投票请求(processRPC)。
还有一个监听的重要 channel,就是这个 heartbeatTimer,就是说说你的集群起来以后应该有一个 timeout 的时间,这个就是一个 channel,经过 timeout,它会发送一个通知。当你 check,如果说没 checkt 没过的话,他会去继续执行到下面,然后执行到这儿(r.setState(Candidate))变成一个 candidate,然后 return 退出这个函数,然后我们再回到这里,你看他就是 return 出来的。
下次他进来就是 candidate。
candidate 里面有什么东西呢,其实也很简单,就是投票的一些信息,可以去看一下。
/* Detailed source-code truncated for AI context efficiency. */先给自己投票(electSelf),然后并行地发送给大家,就是遍历一下当时有哪些服务器,除了自己以外,然后都发出一个,然后返回一个 chanel(voteCh),随时监听这个投票请求的结果。它也需要监听 RPC,因为它也是会收到复制日志的请求,投票的请求,会做相应的处理,并且它主要的工作就是监听这个投票的结果。如果说他的他投票结果,如果说有一个同意了,那么他就会+1,直到他大于等于他的法定人数,或者说 majority 人数的数量,它就会变成 leader,之后就会 return,下次进来他就会进入 leader 的角色。还有一个重要的 channel,就是 electionTimeout,直接 return,进入新一轮选举,还是 candidate,进入新一轮的选举,直到选成了 leader 以后。
然后进入 leader 的逻辑,其实 raft 算法里面核心的逻辑肯定都在 leader。
/* Detailed source-code truncated for AI context efficiency. */这个(setupLeaderState)是初始化一些 leader 的配置,然后接着就是 leader 起来起来以后,(startStopReplication)会给其他其他的每一个节点,启动一个复制的线程,就是说如果有日志复制的消息的话,会发给这些线程,会去执行发送的逻辑。这里面就是启动一个 goroutine,就是异步地启动,然后 replicate 就是整个复制的逻辑就,到时候我们也会详细的讲一下这个方法.
这个(noop)可以单独跟大家说一下,就是这个,每一个 leader 刚开始选举成功以后都会发生一个 noop 的日志,具体大家可以看一下,这个就是日志其实有很多类型,就是用应用程序需要用到一些命令,这个是一个可以操作的日志,他有什么用呢?(下面还有一些其他的日志的类型)它的作用就是其实主要是提交前面任期的 leader 没有提交的日志,这个在论文里面有有提到过。
接下来就是进入 leader 的,它的监听的一些事件,它就监听的稍微多一点,
首先他肯定也是可以监听 RPC 的(processRPC),可以接受投票,复制日志的,但是他只能接受比自己任期大的,接受以后他并不会真正的复制,他会退回到 follower 的状态.后面就是一个重要的是 commitCh,如果说有些提交的消息,会被发送到这个 channel 里,他会监听到,执行 commit 的逻辑,后面就是一个比较重要的是,就是 applyCh,客户端发送的请求会被打包成日志放到这里面来。然后他会做一个 dispatch,就是分发给其他的,刚才我们不是启动了很多复制的线程嘛,这里的话就是分发给那些其他节点。
还有一个就是 lease,他会定期的检查,如果说还是没有得到大多数节点的投票,他就会退回到 follower,逻辑都比较清晰,你是哪个角色都会有相应的逻辑进行执行。
源码讲解的话,后面会根据调试的场景会再次讲解,大致启动的流程和逻辑就是这些。比如说你 New 一个 Raft 以后,刚才的逻辑已经加载进来了,但是因为他们互相之间还连不上,所以会进入这其中一段 Bootstrap,刚才我们把这个集群的信息都加载进来,它就会生成一条配置信息,然后用这个信息来启动集群,启动完成以后。然后就自动地选举出 leader,然后对外提供服务。
调试日志(28:30~)
这个是 raft 的源码的讲解,那么接下来就是第二个重点,就是我们今天调试的一些场景,需要跟大家手把手地调试日志.
先编译,编译完以后,再启动,启动的话先看一下这个命令行,
./raft-demo --http_addr=127.0.0.1:7001 --raft_addr=127.0.0.1:7000 --raft_id=1 --raft_cluster=1/127.0.0.1:7000,2/127.0.0.1:8000,3/127.0.0.1:9000
第一个参数是 HTTP 的地址,因为我写的是微服务服务嘛,所以要需要 http 服务,然后这个是让他们交流的参数就是,网络的地址,后面是 raft 的 ID 节点,每个节点都会需要有一个 ID, 这个 raft_cluster 是整个集群的一个信息。这里我们设置了三个节点,majority 的就是两个节点,只要你有两个节点呢,就处理成功了,或者是处理成功了,就可以提交了,然后执行到状态机,然后并且反馈给客户端成功。
调试场景(28:22~)
选举变化相关(28:22~38:38)
这里的话,我把每个场景都有一个分支都单独的切出来,到时候大家可以看一下.
先看启动场景:
集群启动后,follower 等待一个随机 election timeout 时间变成 candidate,然后发起投票,如果不能获得 majority 票数,则任期 term 会一直增加(未 pre-vote 情况)(branch: election-1)
启动以后,他会发起投票,经历了一个那个 election_timeout 的时间,然后就会到 candidate 的状态,比如说在源码里面的话,他刚开始起来是一个 follower,监听这些 channel,然后都其他 chanel 都没有消息,然后只有这个 heartbeatTimeout,会发送一个消息,他会进行 check,如果没有 leader 对他进行 heartbeat,他就会变成 candidate,然后就会触发新一轮的选举,进入 candidate 的状态,他需要两个投票才行,现在总共才只有一个。因为他根本就联系不上其他两个节点 2 和节点 3,然后就一直这样,26,27(任期),当然之前我也运行过,正常的话它是从 123456, 任期一直增加.当然这是在没有 B 投票的情况,投票其实是对这个产品的一个优化,就是说避免就这个任期一直增加,大概实现的思路就是你要联系上其他的节点,而且你要联系上,然后是能够 ping 到,然后你才会进行投票,大概是这样的一个逻辑啊,大家可以,自己去了解一下。
第二个场景,第二个场景是获得的 majority 的投票.
集群启动后,follower 等待一个随机 election timeout 时间变成 candidate,然后发起投票,获得 majority 票数的节点变成 leader (branch: election-2)
我们运行一下,你看他现在还是联系不上,我们现在再启动一个(节点),那集群里就有两个了,我们看发送投票请求,只有 3 失败了,之前 2 和 3 都失败了,就说明 2 是成功了的,正好这里的日志也印证了,比如说他现在是需要两个,然后他收到了两个,它就会进入 leader 状态,就是说他收到了大多数节点的投票,就会变成 leader。那我们看一下相关的这个逻辑。它是 follower,然后 timeout,然后就会变成 candidate,然后他会发送投票的请求,在这里(runCandidate)就会一直比较,如果说大于等于法定人数的话,那么它就会变成 leader,然后就退出,退出以后,就会执行 leader 的逻辑。
我们看第三个场景。
leader 选举成功后发送 heartbeat 保持 leader 的地位(branch: election-3)
我们把原来 raft 的数据清理一下,启动一个节点,两个节点,我马上给您起来吧。现在 leader 就是上面这个节点,选举成功以后,它会不停的给其他两个节点发送心跳的请求,leader 会给 2,3 发送心跳的信息,保持 leader 地位,其他的节点就不会触发选举。
第四个场景
leader 失去 majority 节点的 heartbeat 响应,退回到 follower(branch: election-4)
这是正常的 heartbeat 的,然后我们两个都停掉,最开始也是很正常的收到请求,后面就是一长串收不到了。失败了嘛,就降级成 follower 的状态,然后又没有其他的 leader 给他发送心跳的信息,然后就会进入 candidate 的状态。然后又发送投票信息,当然现在肯定都是失败的,然后就任期就一直增加,不停的发送投票,我看一下他是从降级的,我们直接看日志吧,到时候大家也可以像我一样看把日志打印出来,这里这样子的,他如果说比如说他联系上的是少于 quorum 的话就会变成 folowwer,我们看一下这个是哪里调用 checkleaderlease 函数的,就是刚才就是 leaderLoop 里面的 lease Channel,它会定时的去检测,如果联系上就没关系,如果联系不上的话就会降级。
这个是四个关于选举的场景,当然可能没有列举完啊,但是我觉得主要的场景其实也就是这些,大家如果根据这些打印的日志的上下文去搜一下打印日志的内容,会有更深的一些理解。
日志复制(38:38~58:40)
后面下面就进入到我们,我们的最重要部分的日志复制。
那现在就是回过回过来讲一下这个图,看一下我们详细的处理客户端请求的逻辑是什么样的
,首先看到这个图,第一步是客户端发送一个请求,发送一个命令给 leader,第二步他的服务层收到以后处理成相关的需要保存的日志数据,然后通过一致性模块,就是 raft replication 一致性模块,第三步就是并行的发送给其他的 follower,有大多数的节点,这里把它置灰了,比如说他宕机掉了,或者说他比较慢,都可以不用管它,只要有一个节点复制成功了,那么他就可以申请第五步提交,提交以后然后会执行到状态机,因为我们执行的状态机,然后都会执行完以后,然后就返回给客户端成功,然后接着就是异步地把提交的信息发送给 follower,
然后 folower 把这个命令到到状态机里面去执行。然后因为你的状态以及初始状态是一致的,然后执行作业命令也是一致的,那么到这一步呢,所以这一刻的状态机的状态肯定都是一致的,都是 358,对吧。那么你集群里面所有节点的数据都就就都保持一致。这里要注意的是就是 7,这个正常来说应该是在 5 之后,就可以把这个信息发送给其他的节点。但是由于就是我们,commit 的策略吧,它是固定一定的时间间隔,把这个提交的信息发送给其他的其他节点,所以从日志上看起来的话,会是像是在返回给客户端以后,到时候我会打印,然后大家也会看到是这样。
(第一个场景)
leader 接收客户端请求,向集群内所有节点发送复制 RPC,所有都正常响应 -> 正常 commit,然后 apply 到状态机,最后返回客户端处理成功(branch: replicate-log-1)
我先模拟客户端,发送一个请求,然后在相应的每一步都打印出来,然后 follower 收到这样的信息,这里的话,我修改了一下,我把 commitTimeout 变成了五秒,正常的话他可能没这么长,他好像原本是 1 秒.
- 第一步发送请求。
- 第二步就是进行到自己的 log 区域里了
- 第三步是两个并行的送给 follower,
- 第四步就是返回,只要有一个返回,他马上就可以提交了
- 第五步是提交 leader 的日志
- 第六步是将日志实时同步到状态机。
- 第七步返回给客户端成功
- 第八步然后就是发送给客户端,提交信息,并且客户端把它更新到状态机里
接下来我跟大家可以跟踪一下这些日志打印的位置,然后大家可以看一下上下文他是如何实现的。
...
(跟着日志讲解日志复制的代码,跳转较多,文字无法充分表述,建议直接参考视频)
...
我们看第二个场景
leader 接收客户端请求,向集群内所有节点发送复制 RPC,majority 正常响应 -> 正常 commit,然后 apply 到状态机,最后返回客户端处理成功(branch: replicate-log-2)
第二个场景就是说,比如说我们三个节点,如果前两个宕掉它能够正常的对外提供服务需求吗?OK 的,你看其实也是 OK 的吧,虽然说咱这个节点宕掉了,但是其实也是 OK 的。
leader 接收客户端请求,向集群内所有节点发送复制 RPC,少于 majority 正常响应 -> 不能 commit(branch: replicate-log-3)
(第三个场景)如果把这两个都宕掉了,然后它只剩 leader,基本上就没办法处理请求了。肯定会失败。所以说节点数小于一半节点数,他就没法工作了。
总结
今天的介绍大概的内容就是这些,然后,我相信这个是分享,主要是跟大家分享一下 raft 的源码啊,还有就是原版里面具体的选举和日志复制的一个实现,大家可以根据这个打印的日志梳理一下上下文,然后又可以更清楚的了解他的一个具体的实现。好的,那这个分享大概就是这些.
QA
- 1.这个库和 ETCD 的库有什么区别?
就说一下共同点吧,就是都是 go 写的,ETCD 那个库的话,他其实只实现了 raft 核心的部分,就是那个最核心的部分的存储和网络传输,那些都交给了,就是用户自己去实现,所以你需要去实现,很多其他的那个工作量,还有一些其他的开发量,比如说保存,存储的发送,都是需要你去实现这些逻辑。但是,还是要客观,那就相当于是全套的.
一整套的你都可以用,也比较简单,可以开箱即用,并且这个 ETCD 这个 raft 库写的更抽象一点,就是把核心算法做出成了一个状态机。他的状态不断的在 ready 里面去暴露,你需要去存储,去发送信息,这个其实是最最主要的一点,就是说,如果说你需要一些高性能的,或者说你需要一些,很多优化的,我觉得可能 ETCD 库会更好一点,但是我觉得如果说你要了解 raft 的算法,去了解 raft 协议的内容,这个库更好一点。
就是说工程上的话,我觉得可能那个 etcd raft 会更好点的就是实际应用。
- 2.怎么做测试?
这个我可能没有更多的经验,我觉得可能还是。就是多预想一些 case 吧,就是比如说,就是你可能自己去构造一个 case 的场景,对写对应的测试用例就可以。
- 3.三个节点,宕机两个,剩下一个不可用,怎么处理请求的强一致?
这个时候服务应该是不可用的,当然如果要强行提供查询的服务,强一致肯定是无法保证的。
- 4.客户单独请求不经过 raft 模块吗?
当然这个的话,大家可以去看一下,consul, 他其实提供了很多的一致性的模型呢,就是你可能不需要强一致性,也需要强一致性,他有不同的模型,如果不需要强一致性,那你可以不需要经过任何的模块,如果说你需要强一致性,那最简单的就是通过 raft 的模块,就是你的日志复制到了大多数节点上,那么你才给客户端返回,这个是最简单的,其他的可能还有一些更优雅或者更好的设计,这个大家可以去了解下
- 5.这个库状态机实现不需要暴露读接口吗?
对,其实这两个我觉得这三个吧,其实都是关于一致性的问题,其实都都算是一个问题吧,
其实那个 apply 那其实你也可以读,也可以写啊,都可以。
- 6.节点之间的长连接怎么建立并通信,源码在哪儿?
在这个文件(raft-demo/net_transport.go)里面,他是单独的一个模块写的,都在这里面,他怎么样做去做 RPC 的分发,接收响应都在里面,大家可以,你们可以去看一下。
- 7.第七步未成功,leader 会给 client 正常返回吗?
那肯定肯定是不会啊,不会返回的呀,你没处理成功,肯定不会。
但是对 leader,其实他收不到吗?收不到就是 timeout 了,正常的话,你他客户端 timeout 了,你肯定会去执行一下你客户端的逻辑,你比如说正常的请求,就是你客户端 timeout 了,那我肯定不会,不可能认为他是执行完成或者没完成,那可能会有一些一次查询什么的,所以你的客户端会有一定的开发工作量,大概他在这这里(论文)有一些简单的简单的介绍啊,你可以看一下,客户端开发会有一些,比如说怎么样防止
重复请求,涉及到客户端一些的开发的工作量,那是肯定是不可能,肯定就不会返回成功。
超时的话,leader 肯定是反馈给一个错误,就是请求请求失败给客户端。但是他会知道是,比如说看你看这个,不是有个 timeout 的实现吗?那你肯定知道是 timeout 的失败的,那么其实对于我客户端的视角来说, timeout 其实我并不知道这个请求是处理成功还是处理,(失败了对吧?)
我不能默认他成功,也不能默认他失败,那我对于我客户端,我是不是得去查询一下之类的一些动作。失败了,客户端会反馈给他失败的,实际有没有执行的话,可能你需要去,通过业务场景去查询一下,我认为是这样子的。
- 8.你在 debug 的这个库的这个 raft 的算法你大概用了多长时间?这样也可以帮助,现在可能还没有去学,但是可能想要去学,他可以预估一下这个大概的时间?
就是首先要去看这个源码,要去做这个的 debug 的话,肯定大家要对 raft 协议有一个那个最基本的认识,就是至少这个论文我不管中文还是英文,应该有有读,过了时间的话可能跟每个人不同,以我自己的话,论文我大概前前后后大概看了有两三个月吧,也不是一直在看,就偶尔有空看一下就是,反正我是对照中英文,然后以英文为主,然后再来看。看完以后,然后就当时确实也比较懵逼吧,然后就想着是要看一下源码实现才会更深刻地理解,就找到了这个库,然后不是直接就看库,然后就看会搜一些,简单的,就像我的特约服务一样,其实别人也有写的,就像我刚才那种那种跟踪跟踪代码的方法,然后去一步一步的那个找到他的关键的逻辑,大概的话,看源码的话,其实如果你了解是了解的那个协议的话,就稍微比较快一点,我觉得可能就是可能几周吧,或者两三周我觉得差不多你就可以,你就可以看明白了。
Night 105 2020 10 03 Go Zero Discuss (`content/night/105-2020-10-03-go-zero-discuss.md`)
desc: Go 夜读之 go-zero 微服务框架和线上答疑 title: 第 105 期 go-zero 微服务框架和线上答疑 date: 2020-10-03T20:00:00+08:00 author: 万俊峰(Kevin)@晓黑板
1. go-zero 适用场景
1.1 希望说说应用场景,各个场景下的优势
- 高并发的微服务系统
- 支撑千万级日活,百万级 QPS
- 完整的微服务治理能力
- 支持自定义中间件
- 很好的管理了数据库和缓存
- 有效隔离故障
- 低并发的单体系统
- 这种系统直接使用 api 层即可,无需 rpc 服务
1.2 各个功能的使用场景以及使用案例
- 限流
- 熔断
- 降载
- 超时
- 可观测性
2. go-zero 的实际体验
- 服务很稳
- 前后端接口一致性,一个 api 文件即可生成前后端代码
- 规范、代码量少,意味着 bug 少
- 免除 api 文档,极大降低沟通成本
- 代码结构完全一致,便于维护和接手
3. 微服务的项目结构, monorepo 的 CICD 处理 bookstore
├── api
│ ├── etc
│ └── internal
│ ├── config
│ ├── handler
│ ├── logic
│ ├── svc
│ └── types
└── rpc
├── add
│ ├── adder
│ ├── etc
│ ├── internal
│ │ ├── config
│ │ ├── logic
│ │ ├── server
│ │ └── svc
│ └── pb
├── check
│ ├── checker
│ ├── etc
│ ├── internal
│ │ ├── config
│ │ ├── logic
│ │ ├── server
│ │ └── svc
│ └── pb
└── modelmono repo 的 CI 我们是通过 gitlab 做的,CD 使用 jenkins
CI 尽可能更严格的模式,比如 - race,使用 sonar 等工具
CD 有开发、测试、预发、灰度和正式集群
晚 6 点上灰度、无故障的话第二天 10 点自动同步到正式集群
正式集群分为多个 k8s 集群,有效的防止单集群故障,直接摘除即可,集群升级更有好
4. 如何部署,如何监控?
- 全量 k8s,通过 jenkins 自动打包成 docker 镜像,按照时间打包 tag,这样可以一眼看出哪一天的镜像
- 上面已经讲了,预发 -> 灰度 -> 正式
- Prometheus + 自建 dashboard 服务
- 基于日志检测服务和请求异常
5. 如果打算换 go-zero 框架重构业务,如何做好线上业务稳定安全用户无感切换?另外咨询下如何进行服务划分?
- 逐步替换,从外到内,加个 proxy 来校对,校对一周后可以切换
- 如有数据库重构,则需要做好新老同步
- 服务划分按照业务来,遵循从粗到细的原则,避免一个 api 一个微服务
- 数据拆分对于微服务来讲尤为重要,上层好拆,数据难拆,尽可能保证按照业务拆分数据
6. 服务发现
6.1 服务发现 etcd 的 key 的设计
- 服务 key + 时间戳,服务进程数存在时间戳冲突的概率极低,忽略
6.2 etcd 服务发现与治理, 异常捕获与处理异常
- 为啥 k8s 还使用 etcd 做服务发现,因为 dns 的刷新有延迟,导致滚动更新会有大量失败,而 etcd 可以做到完全无损更新
- etcd 集群直接部署在 k8s 集群内,因为多个正式集群,集群单点和注册避免混乱
- 针对 etcd 异常或者 leader 切换,自动侦测并刷新,当 etcd 有异常不能恢复时,不会刷新服务列表,保障服务依然可用
7. 缓存的设计与使用案例
- 分布式多 redis 集群,线上最大几十个集群为同一个服务提供缓存服务
- 无缝扩缩容
- 不存在没有过期时间的缓存,避免大量不常使用的数据占用资源,默认一周
- 缓存穿透,没有的数据会短暂缓存一分钟,避免刷接口或大量不存在的数据请求带垮系统
- 缓存击穿,一个进程只会刷新一次同一个数据,避免热点数据被大量同时加载
- 缓存雪崩,对缓存过期时间自动做了 jitter,5% 的标准变差,使得一周的过期时间分布在 16 小时内,有效防止了雪崩
- 我们线上数据库都有缓存,否则无法支撑海量并发
- 自动缓存管理已经内置于 go-zero,并可以通过 goctl 自动生成代码
8. 中间件,拦截器的设计思想
- 洋葱模型
- 本中间件处理,比如限流,熔断等,然后决定是否调用 next
- next 调用
- 对 next 调用返回结果做处理
9. 微服务的事务处理怎么实现比较好?go-zero 分布式事务设计和实现,有什么好中间件推荐?
- 2PC,两阶段提交
- TCC,Try-Confirm-Cancel
- 消息队列,最大尝试
- 人工补偿
10. 多级 goroutine 的异常捕获 ,怎么设计比较好
- 微服务系统请求异常应该隔离,不能让单个异常请求带崩整个进程
- go-zero 自带了 RunSafe/GoSafe,用来防止单个异常请求导致进程崩溃
- 监控需要跟上,防止异常过量而不自知
- fail fast 和故障隔离的矛盾点
11. k8s 配置的生成与使用 (gateway, service, slb)
- 内部自动生成 k8s 的 yaml 文件,过于依赖配置而未开源
- 打算在 bookstore 的示例里加上 k8s 配置样板
- slb->nginx->nodeport->api gateway->rpc service
12. gateway 限流、熔断和降载
- 限流分为两种:并发控制和分布式限流
- 并发控制用来防止瞬间过量请求,保护系统不被打垮
- 分布式限流用来给不同服务配置不同的 quota
- 熔断是为了对依赖的服务进行保护,当一个服务出现大量异常的时候,调用者应该给予保护,使其有机会恢复正常,同时也达到 fail fast 的效果
- 降载是为了保护当前进程资源耗尽而陷入彻底不可用,确保尽可能服务好能承载的最大请求量
- 降载配合 k8s,可以有效保护 k8s 扩容,k8s 扩容分钟级,go-zero 降载秒级
13. 介绍 core 中好用的组件,如 timingwheel 等,讲讲设计思路
- 布隆过滤器
- 进程内 cache
- RollingWindow
- TimingWheel
- 各种 executors
- fx 包,map/reduce/filter/sort/group/distinct/head/tail...
- 一致性 hash 实现
- 分布式限流实现
- mapreduce,带 cancel 能力
- syncx 包里有大量的并发工具
14. 如何快速增加一种 rpc 协议支持,將跨机发现改为调本机节点,并关闭复杂 filter 和负载均衡功能
- go-zero 跟 grpc 关系还是比较紧密的,设计之初没有考虑支持 grpc 以外的协议
- 如果要增加的话,那就只能 fork 出来魔改了
- 调本机直接用 direct 的 scheme 即可
- 为啥要去掉 filter 和负载均衡?如果要去的话,fork 了改,但没必要
15. 日志和监控和链路追踪的设计和实现思路,最好有大概图解
- 日志和监控我们使用 prometheus, 自定义 dashboard 服务,捆绑提交数据(每分钟)
- 链路追踪可以看出调用关系,自动记录 trace 日志
16. go-zero 框架有用到什么池化技术吗?如果有,在哪些 core 代码里面可以参考
- 一般不需要提前优化,过度优化是大忌
- core/syncx/pool.go 里面定义了带过期时间的通用池化技术
17. go-zero 用到了那些性能测试方法框架,有代码参考吗?可以说说思路和经验
- go benchmark
- 压测可以通过现有业务日志样本,来按照预估等比放大
- 压测一定要压到系统扛不住,看第一个瓶颈在哪里,改完再压,循环
18. 说一下代码的抽象经验和心得
- Don’t repeat yourself
- 你未必需要它,之前经常有业务开发人员问我可不可以增加这个功能或那个功能,我一般都会仔细询问深层次目的,很多时候会发现其实这个功能是多余的,不需要才是最佳实践
- Martin Fowler 提出出现三次再抽象的原则,有时有些同事会找我往框架里增加一个功能,我思考后经常会回答这个你先在业务层写,其它地方也有需要了你再告诉我,三次出现我会考虑集成到框架里
- 一个文件应该尽量只做一件事,每个文件尽可能控制在 200 行以内,一个函数尽可能控制在 50 行以内,这样不需要滚动就可以看到整个函数
- 需要抽象和提炼的能力,多去思考,经常回头思考之前的架构或实现
19. 你会就 go-zero 框架从设计到实践出书吗?框架以后的发展规划是什么?
- 暂无出书计划,做好框架是最重要的
- 继续着眼于工程效率
- 提升服务治理能力
- 帮助业务开发尽可能快速落地
资料
- https://talkgo.org/t/topic/1070
- https://github.com/tal-tech/go-zero
- https://www.yuque.com/tal-tech/go-zero
Night 11 2018 07 26 Golang Jenkins Sonarqube (`content/night/11-2018-07-26-golang-jenkins-sonarqube.md`)
title: 第 11 期 Golang 代码质量持续检测实践 date: 2018-07-26T00:00:00+08:00
观看视频
{{< youtube id="d95PZDAabqQ" >}}
Night 12 2018 08 02 Goroutine GPM (`content/night/12-2018-08-02-goroutine-GPM.md`)
title: 第 12 期 golang 中 goroutine 的调度 date: 2018-08-02T11:49:10+08:00
郑宝杨(boya) 2018-08-01 [email protected]
阅读源码前可以阅读的资料
golang的调度模型概览
调度的机制用一句话描述:
runtime准备好G,P,M,然后M绑定P,M从各种队列中获取G,切换到G的执行栈上并执行G上的任务函数,调用goexit做清理工作并回到M,如此反复。
基本概念
M(machine)
- M代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。
- M是真正调度系统的执行者,每个M就像一个勤劳的工作者,总是从各种队列中找到可运行的G,而且这样M的可以同时存在多个。
- M在绑定有效的P后,进入调度循环,而且M并不保留G状态,这是G可以跨M调度的基础。
P(processor)
- P表示逻辑processor,是线程M的执行的上下文。
- P的最大作用是其拥有的各种G对象队列、链表、cache和状态。
G(goroutine)
- 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
- 在G的眼中只有P,P就是运行G的“CPU”。
- 相当于两级线程
线程实现模型
来自Go并发编程实战
+-------+ +-------+
| KSE | | KSE |
+-------+ +-------+
| | 内核空间
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| | 用户空间
+-------+ +-------+
| M | | M |
+-------+ +-------+
| | | |
+------+ +------+ +------+ +------+
| P | | P | | P | | P |
+------+ +------+ +------+ +------+
| | | | | | | | |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
| G | | G | | G | | G | | G | | G | | G | | G | | G |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+- KSE(Kernel Scheduling Entity)是内核调度实体
- M与P,P与G之前的关联都是动态的,可以变的
关系示意图
来自golang源码剖析
+-------------------- sysmon ---------------//------+
| |
| |
+---+ +---+-------+ +--------+ +---+---+
go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
+---+ +---+-------+ +--------+ +---+---+
| | |
| +---+ | |
+----> | M | <--- findrunnable ---+--- steal <--//--+
+---+
|
mstart
|
+--- execute <----- schedule
| |
| |
+--> G.fn --> goexit --+
1. go func() 语气创建G。
2. 将G放入P的本地队列(或者平衡到全局全局队列)。
3. 唤醒或新建M来执行任务。
4. 进入调度循环
5. 尽力获取可执行的G,并执行
6. 清理现场并且重新进入调度循环GPM的来由
特殊的g0和m0
g0和m0是在proc.go文件中的两个全局变量,m0就是进程启动后的初始线程,g0也是代表着初始线程的stackasm_amd64.go --> runtime·rt0_go(SB)
// 程序刚启动的时候必定有一个线程启动(主线程)
// 将当前的栈和资源保存在g0
// 将该线程保存在m0
// tls: Thread Local Storage
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX)
LEAQ runtime·m0(SB), AX
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
MOVQ AX, g_m(CX)M的一生
M的创建
proc.go
// Create a new m. It will start off with a call to fn, or else the scheduler.
// fn needs to be static and not a heap allocated closure.
// May run with m.p==nil, so write barriers are not allowed.
//go:nowritebarrierrec
// 创建一个新的m,它将从fn或者调度程序开始
func newm(fn func(), _p_ *p) {
// 根据fn和p和绑定一个m对象
mp := allocm(_p_, fn)
// 设置当前m的下一个p为_p_
mp.nextp.set(_p_)
mp.sigmask = initSigmask
...
// 真正的分配os thread
newm1(mp)
}func newm1(mp *m) {
// 对cgo的处理
...
execLock.rlock() // Prevent process clone.
// 创建一个系统线程
newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
execLock.runlock()
}状态
mstart
|
v 找不到可执行任务,gc STW,
+------+ 任务执行时间过长,系统阻塞等 +------+
| spin | ----------------------------> |unspin|
+------+ mstop +------+
^ |
| v
notewakeup <------------------------- notesleepM的一些问题
https://github.com/golang/go/issues/14592
P的一生
P的创建
proc.go
/* Detailed source-code truncated for AI context efficiency. */所有的P在程序启动的时候就设置好了,并用一个allp slice维护,可以调用runtime.GOMAXPROCS调整P的个数,虽然代价很大
状态转换
acquirep(p)
不需要使用的P P和M绑定的时候 进入系统调用 procresize()
new(p) -----+ +---------------+ +-----------+ +------------+ +----------+
| | | | | | | | |
| +------------+ +---v--------+ +---v--------+ +----v-------+ +--v---------+
+-->| _Pgcstop | | _Pidle | | _Prunning | | _Psyscall | | _Pdead |
+------^-----+ +--------^---+ +--------^---+ +------------+ +------------+
| | | | | |
+------------+ +------------+ +------------+
GC结束 releasep() 退出系统调用
P和M解绑P的数量默认等于cpu的个数,很多人认为runtime.GOMAXPROCS可以限制系统线程的数量,但这是错误的,M是按需创建的,和runtime.GOMAXPROCS没有关系。
G的一生
G的创建
proc.go
// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
// 新建一个goroutine,
// 用fn + PtrSize 获取第一个参数的地址,也就是argp
// 用siz - 8 获取pc地址
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
pc := getcallerpc()
// 用g0的栈创建G对象
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, pc)
})
}/* Detailed source-code truncated for AI context efficiency. */G的状态图
+------------+
ready | |
+------------------ | _Gwaiting |
| | |
| +------------+
| ^ park_m
V |
+------------+ +------------+ execute +------------+ +------------+
| | newproc | | ---------> | | goexit | |
| _Gidle | ---------> | _Grunnable | yield | _Grunning | ---------> | _Gdead |
| | | | <--------- | | | |
+------------+ +-----^------+ +------------+ +------------+
| entersyscall | ^
| V | existsyscall
| +------------+
| existsyscall | |
+------------------ | _Gsyscall |
| |
+------------+新建的G都是_Grunnable的,新建G的时候优先从gfree list从获取G,这样可以复用G,所以上图的状态不是完整的,_Gdead通过newproc会变为_Grunnable,
通过go func()的语法新建的G,并不是直接运行,而是放入可运行的队列中,什么时候运行用于并不能决定,而是搞调度系统去自发的运行。
观看视频
{{< youtube id="98pIzaOeD2k" >}}
Night 13 2018 08 09 Kubernetes Guide (`content/night/13-2018-08-09-kubernetes-guide.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 13 期 Kubernetes 入门指南 date: 2018-08-09T00:00:08+08:00 author: 李森森
观看视频
{{< youtube id="DJgYlmGCmDA" >}}
Night 14 2018 08 17 Sync Pool Reading (`content/night/14-2018-08-17-sync-pool-reading.md`)
title: 第 14 期 sync.Pool 源码分析及适用场景 date: 2018-08-17T11:49:10+08:00
Go 标准包阅读
观看视频
{{< youtube id="jaepwn2PWPk" >}}
Night 15 2018 08 23 Pool Workshop In Go (`content/night/15-2018-08-23-pool-workshop-in-go.md`)
title: 第 15 期 多路复用资源池组件剖析 date: 2018-08-23T11:49:10+08:00
2018-08-23 22:00:00 分享会之后的答疑。
源代码地址:pool#workshop
一个网友在分享会之后的个人理解:对是独占资源对象的复用,提升了最后的 qps,独占式方法 TestChanPool() 函数中使用了从资源池获取 worker 对象,执行完毕后再放回资源池,如果获取不到则阻塞等待,因此,100 000 请求,每个请求占用 10ms,可用 worker 对象 50 个,则最后 100 000*10/50 =20s ,视频中测试结果也显示 21s 符合预期。而 TestWorkshop() 函数中使用回调函数对 worker 进行加锁,每个线程使用的那一刻是 worker 对象是被独占的,而后续的 do{sleep(10ms)} 是并发执行的,并且根据每个 worker 同时执行的 do 的任务数,进行负载均衡,所以最后测试性能 QPS 能够有 20 倍的提升。
workshop 中每个协程只在获得 worker 的那一刻是互斥的,且不会从池子中移除,通过状态统计达到资源的负载均衡。在业务上真正使用资源时其实是无锁状态,所以能被其他协程同时使用,进而吞吐量提升。业务逻辑耗时越长,相比独占式资源池的吞吐量优势越显著。本机测试 50 个资源 10ms 时可提升 20 倍。
为什么不使用轮询使用资源,代码实现会更简单?
实际场景中每次业务逻辑耗时不相同,轮询并不能保证真的负载均衡。尤其是当突发异常时,可能导致负载失衡。
为什么长连接异步通信不使用一条连接而是连接池?
说到长连接异步通信,为什么不使用一条连接而是连接池,其实涉及到多条连接抢占带宽和 TCP 丢包后速率下降的问题。这对于下载场景(迅雷就是这么做的)和使用共享云主机的场景比较有用。
具体可以看这篇文章:为什么多 TCP 连接比单TCP连接传输快
以前在做加速的一项就是多线程下载,开启多个 tcp 连接,同时下载,比只有一个连接下载快多了。
workshop 的使用场景
使用 workshop 的前提就是该资源可以被同时使用,比如长连接的异步IO通信。
对于长连接异步通信时,如果使用了独占式连接池只会起到反效果,让它和同步通信没差别,还不如不用池子。workshop 就是适用于这个场景的。
长连接:同步和异步方式。
同步方式下客户端所有请求共用同一连接,在获得连接后要对连接加锁,在读写结束后才解锁释放连接,性能低下,基本很少采用,唯一优点是实现极其简单。
异步方式下所有请求都带有消息ID,因此可以批量发送请求,异步接收回复,所有请求和回复的消息都共享同一连接,信道得到最大化利用,因此吞吐量最大。
这个时候接收端的处理能力也要求比较高,一般都是独立的一个(或者多个)收包线程(或者进程)防止内核缓冲区被填满影响网络吞吐量。缺点是实现复杂,需要异步状态机,需要增加负载均衡和连接健康度检测机制,等等。
workshop 就是实现了上述多条异步连接间的负载均衡,健康检测等。
观看视频
{{< youtube id="CDfrRzgmR4E" >}}
参考资料
Night 16 2018 09 06 Gateway Reading (`content/night/16-2018-09-06-gateway-reading.md`)
title: 第 16 期 gateway-reading date: 2018-09-06T11:49:10+08:00 tags:
OpenFaaS的Gateway是一个golang实现的请求转发的网关,在这个网关服务中,主要有以下几个功能:
- UI
- 部署函数
- 监控
- 自动伸缩
架构分析
从图中可以发现,当Gateway作为一个入口,当CLI或者web页面发来要部署或者调用一个函数的时候,Gateway会将请求转发给Provider,同时会将监控指标发给Prometheus。AlterManager会根据需求,调用API自动伸缩函数。
源码分析
依赖
github.com/gorilla/mux
github.com/nats-io/go-nats-streaming
github.com/nats-io/go-nats
github.com/openfaas/nats-queue-worker
github.com/prometheus/client_golangmux 是一个用来执行http请求的路由和分发的第三方扩展包。
go-nats-streaming,go-nats,nats-queue-worker这三个依赖是异步函数的时候才会用到,在分析queue-worker的时候有说到Gateway也是一个发布者。
client_golang是Prometheus的客户端。
项目结构
├── Dockerfile
├── Dockerfile.arm64
├── Dockerfile.armhf
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── assets
├── build.sh
├── handlers
│ ├── alerthandler.go
│ ├── alerthandler_test.go
│ ├── asyncreport.go
│ ├── baseurlresolver_test.go
│ ├── basic_auth.go
│ ├── basic_auth_test.go
│ ├── callid_middleware.go
│ ├── cors.go
│ ├── cors_test.go
│ ├── forwarding_proxy.go
│ ├── forwarding_proxy_test.go
│ ├── function_cache.go
│ ├── function_cache_test.go
│ ├── infohandler.go
│ ├── metrics.go
│ ├── queueproxy.go
│ ├── scaling.go
│ └── service_query.go
├── metrics
│ ├── add_metrics.go
│ ├── add_metrics_test.go
│ ├── externalwatcher.go
│ ├── metrics.go
│ └── prometheus_query.go
├── plugin
│ ├── external.go
│ └── external_test.go
├── queue
│ └── types.go
├── requests
│ ├── forward_request.go
│ ├── forward_request_test.go
│ ├── prometheus.go
│ ├── prometheus_test.go
│ └── requests.go
├── server.go
├── tests
│ └── integration
├── types
│ ├── handler_set.go
│ ├── inforequest.go
│ ├── load_credentials.go
│ ├── proxy_client.go
│ ├── readconfig.go
│ └── readconfig_test.go
├── vendor
│ └── github.com
└── version
└── version.goGateway的目录明显多了很多,看源码的时候,首先要找到的是main包,从main函数看起,就能很容易分析出来项目是如何运行的。
从server.go的main函数中我们可以看到,其实有如下几个模块:
- 基本的安全验证
- 和函数相关的代理转发
- 同步函数
- 列出函数
- 部署函数
- 删除函数
- 更新函数
- 异步函数
- 同步函数
- Prometheus的监控
- ui
- 自动伸缩
基本的安全验证
如果配置了开启基本安全验证,会从磁盘中读取密钥:
var credentials *types.BasicAuthCredentials
if config.UseBasicAuth {
var readErr error
reader := types.ReadBasicAuthFromDisk{
SecretMountPath: config.SecretMountPath,
}
credentials, readErr = reader.Read()
if readErr != nil {
log.Panicf(readErr.Error())
}
}在Gateway的配置相关的,都会有一个read()方法,进行初始化赋值。
如果credentials被赋值之后,就会对一些要加密的API handler进行一个修饰,被修饰的API有:
- UpdateFunction
- DeleteFunction
- DeployFunction
- ListFunctions
- ScaleFunction
if credentials != nil {
faasHandlers.UpdateFunction =
handlers.DecorateWithBasicAuth(faasHandlers.UpdateFunction, credentials)
faasHandlers.DeleteFunction =
handlers.DecorateWithBasicAuth(faasHandlers.DeleteFunction, credentials)
faasHandlers.DeployFunction =
handlers.DecorateWithBasicAuth(faasHandlers.DeployFunction, credentials)
faasHandlers.ListFunctions =
handlers.DecorateWithBasicAuth(faasHandlers.ListFunctions, credentials)
faasHandlers.ScaleFunction =
handlers.DecorateWithBasicAuth(faasHandlers.ScaleFunction, credentials)
}这个DecorateWithBasicAuth()方法是一个路由中间件:
- 调用mux路由的BasicAuth(),从http的header中取到用户名和密码
- 然后给请求头上设置一个字段
WWW-Authenticate,值为Basic realm="Restricted" - 如果校验失败,则返回错误,成功的话调用next方法继续进入下一个handler。
// DecorateWithBasicAuth enforces basic auth as a middleware with given credentials
func DecorateWithBasicAuth(next http.HandlerFunc, credentials *types.BasicAuthCredentials) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, password, ok := r.BasicAuth()
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
if !ok || !(credentials.Password == password && user == credentials.User) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("invalid credentials"))
return
}
next.ServeHTTP(w, r)
}
}代理转发
Gateway本身不做任何和部署发布函数的事情,它只是作为一个代理,把请求转发给相应的Provider去处理,所有的请求都要通过这个网关。
同步函数转发
主要转发的API有:
- RoutelessProxy
- ListFunctions
- DeployFunction
- DeleteFunction
- UpdateFunction
faasHandlers.RoutelessProxy = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)
faasHandlers.ListFunctions = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)
faasHandlers.DeployFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)
faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)
faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)MakeForwardingProxyHandler()有三个参数:
proxy
这是一个http的客户端,作者把这个客户端抽成一个类,然后使用该类的NewHTTPClientReverseProxy方法创建实例,这样就简化了代码,不用每次都得写一堆相同的配置。
notifiers
这个其实是要打印的日志,这里是一个HTTPNotifier的接口。而在这个MakeForwardingProxyHandler中其实有两个实现类,一个是LoggingNotifier,一个是PrometheusFunctionNotifier,分别用来打印和函数http请求相关的日志以及和Prometheus监控相关的日志。
baseURLResolver
这个就是Provider的url地址。
在这个MakeForwardingProxyHandler中主要做了三件事儿:
解析要转发的url
调用forwardRequest方法转发请求,
forwardRequest方法的逻辑比较简单,只是把请求发出去。这里就不深入分析了。
打印日志
// MakeForwardingProxyHandler create a handler which forwards HTTP requests
func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier, baseURLResolver BaseURLResolver) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
baseURL := baseURLResolver.Resolve(r)
requestURL := r.URL.Path
start := time.Now()
statusCode, err := forwardRequest(w, r, proxy.Client, baseURL, requestURL, proxy.Timeout)
seconds := time.Since(start)
if err != nil {
log.Printf("error with upstream request to: %s, %s\n", requestURL, err.Error())
}
for _, notifier := range notifiers {
notifier.Notify(r.Method, requestURL, statusCode, seconds)
}
}
}异步函数转发
前面说过,如果是异步函数,Gateway就作为一个发布者,将函数放到队列里。MakeQueuedProxy方法就是做这件事儿的:
- 读取请求体
- 将
X-Callback-Url参数从参数中http的header中读出来 - 实例化用于异步处理的Request对象
- 调用canQueueRequests.Queue(req),将请求发布到队列中
// MakeQueuedProxy accepts work onto a queue
func MakeQueuedProxy(metrics metrics.MetricOptions, wildcard bool, canQueueRequests queue.CanQueueRequests) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
// 省略错误处理代码
vars := mux.Vars(r)
name := vars["name"]
callbackURLHeader := r.Header.Get("X-Callback-Url")
var callbackURL *url.URL
if len(callbackURLHeader) > 0 {
urlVal, urlErr := url.Parse(callbackURLHeader)
// 省略错误处理代码
callbackURL = urlVal
}
req := &queue.Request{
Function: name,
Body: body,
Method: r.Method,
QueryString: r.URL.RawQuery,
Header: r.Header,
CallbackURL: callbackURL,
}
err = canQueueRequests.Queue(req)
// 省略错误处理代码
w.WriteHeader(http.StatusAccepted)
}
}自动伸缩
伸缩性其实有两种,一种是可以通过调用API接口,来将函数进行缩放。另外一种就是通过AlertHandler。
自动伸缩是OpenFaaS的一大特点,触发自动伸缩主要是根据不同的指标需求。
根据每秒请求数来做伸缩
OpenFaaS附带了一个自动伸缩的规则,这个规则是在AlertManager配置文件中定义。AlertManager从Prometheus中读取使用情况(每秒请求数),然后在满足一定条件时向Gateway发送警报。
可以通过删除AlertManager,或者将部署扩展的环境变量设置为0,来禁用此方式。
最小/最大副本数
通过向函数添加标签, 可以在部署时设置最小 (初始) 和最大副本数。
com.openfaas.scale.min默认是1com.openfaas.scale.max默认是20com.openfaas.scale.factor默认是20%,在0-100之间,这是每次扩容的时候,新增实例的百分比,若是100的话,会瞬间飙升到副本数的最大值。
com.openfaas.scale.min和com.openfaas.scale.max值一样的时候,可以关闭自动伸缩。com.openfaas.scale.factor是0时,也会关闭自动伸缩。通过内存和CPU的使用量。
使用k8s内置的HPA,也可以触发AlertManager。
手动指定伸缩的值
可以从这句代码中发现,调用这个路由,转发给了provider处理。
r.HandleFunc("/system/scale-function/{name:[-a-zA-Z_0-9]+}", faasHandlers.ScaleFunction).Methods(http.MethodPost)处理AlertManager的伸缩请求
Prometheus将监控指标发给AlertManager之后,会触发AlterManager调用/system/alert接口,这个接口的handler是由handlers.MakeAlertHandler方法生成。
MakeAlertHandler方法接收的参数是ServiceQuery。ServiceQuery是一个接口,它有两个函数,用来get或者ser最大的副本数。Gateway中实现这个接口的类是ExternalServiceQuery,这个实现类是在plugin包中,我们也可以直接定制这个实现类,用来实现满足特定条件。
// ServiceQuery provides interface for replica querying/setting
type ServiceQuery interface {
GetReplicas(service string) (response ServiceQueryResponse, err error)
SetReplicas(service string, count uint64) error
}
// ExternalServiceQuery proxies service queries to external plugin via HTTP
type ExternalServiceQuery struct {
URL url.URL
ProxyClient http.Client
}这个ExternalServiceQuery有一个NewExternalServiceQuery方法,这个方法也是一个工厂方法,用来创建实例。这个url其实就是provider的url,proxyClient就是一个http的client对象。
GetReplicas方法从
system/function/:name接口获取到函数的信息,组装一个ServiceQueryResponse对象即可。SetReplicas方法调用
system/scale-function/:name接口,设置副本数。
MakeAlertHandler的函数主要是从http.Request中读取body,然后反序列化成PrometheusAlert对象:
// PrometheusAlert as produced by AlertManager
type PrometheusAlert struct {
Status string `json:"status"`
Receiver string `json:"receiver"`
Alerts []PrometheusInnerAlert `json:"alerts"`
}可以发现,这个Alerts是一个数组对象,所以可以是对多个函数进行缩放。反序列化之后,调用handleAlerts方法,而handleAlerts对Alerts进行遍历,针对每个Alerts调用了scaleService方法。scaleService才是真正处理伸缩服务的函数。
func scaleService(alert requests.PrometheusInnerAlert, service ServiceQuery) error {
var err error
serviceName := alert.Labels.FunctionName
if len(serviceName) > 0 {
queryResponse, getErr := service.GetReplicas(serviceName)
if getErr == nil {
status := alert.Status
newReplicas := CalculateReplicas(status, queryResponse.Replicas, uint64(queryResponse.MaxReplicas), queryResponse.MinReplicas, queryResponse.ScalingFactor)
log.Printf("[Scale] function=%s %d => %d.\n", serviceName, queryResponse.Replicas, newReplicas)
if newReplicas == queryResponse.Replicas {
return nil
}
updateErr := service.SetReplicas(serviceName, newReplicas)
if updateErr != nil {
err = updateErr
}
}
}
return err
}从代码总就可以看到,scaleService做了三件事儿:
获取现在的副本数
计算新的副本数
新副本数的计算方法是根据
com.openfaas.scale.factor计算步长:gostep := uint64((float64(maxReplicas) / 100) * float64(scalingFactor))设置为新的副本数
从0增加副本到的最小值
我们在调用函数的时候,用的路由是:/function/:name。如果环境变量里有配置scale_from_zero为true,先用MakeScalingHandler()方法对proxyHandler进行一次包装。
MakeScalingHandler接受参数主要是:
next:就是下一个httpHandlerFunc,中间件都会有这样一个参数
config:
ScalingConfig的对象:go// ScalingConfig for scaling behaviours type ScalingConfig struct { MaxPollCount uint // 查到的最大数量 FunctionPollInterval time.Duration // 函数调用时间间隔 CacheExpiry time.Duration // 缓存过期时间 ServiceQuery ServiceQuery // 外部服务调用的一个接口 }
这个MakeScalingHandler中间件主要做了如下的事情:
- 先从FunctionCache缓存中获取该函数的基本信息,从这个缓存可以拿到每个函数的副本数量。
- 为了加快函数的启动速度,如果缓存中可以获该得函数,且函数的副本数大于0,满足条件,return即可。
- 如果不满足上一步,就会调用
SetReplicas方法设置副本数,并更新FunctionCache的缓存。
// MakeScalingHandler creates handler which can scale a function from
// zero to 1 replica(s).
func MakeScalingHandler(next http.HandlerFunc, upstream http.HandlerFunc, config ScalingConfig) http.HandlerFunc {
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: config.CacheExpiry,
}
return func(w http.ResponseWriter, r *http.Request) {
functionName := getServiceName(r.URL.String())
if serviceQueryResponse, hit := cache.Get(functionName); hit && serviceQueryResponse.AvailableReplicas > 0 {
next.ServeHTTP(w, r)
return
}
queryResponse, err := config.ServiceQuery.GetReplicas(functionName)
cache.Set(functionName, queryResponse)
// 省略错误处理
if queryResponse.AvailableReplicas == 0 {
minReplicas := uint64(1)
if queryResponse.MinReplicas > 0 {
minReplicas = queryResponse.MinReplicas
}
err := config.ServiceQuery.SetReplicas(functionName, minReplicas)
// 省略错误处理代码
for i := 0; i < int(config.MaxPollCount); i++ {
queryResponse, err := config.ServiceQuery.GetReplicas(functionName)
cache.Set(functionName, queryResponse)
// 省略错误处理
time.Sleep(config.FunctionPollInterval)
}
}
next.ServeHTTP(w, r)
}
}监控
监控是一个定时任务,开启了一个新协程,利用go的ticker.C的间隔不停的去调用/system/functions接口。反序列化到MetricOptions对象中。
func AttachExternalWatcher(endpointURL url.URL, metricsOptions MetricOptions, label string, interval time.Duration) {
ticker := time.NewTicker(interval)
quit := make(chan struct{})
proxyClient := // 省略创建一个http.Client对象
go func() {
for {
select {
case <-ticker.C:
get, _ := http.NewRequest(http.MethodGet, endpointURL.String()+"system/functions", nil)
services := []requests.Function{}
res, err := proxyClient.Do(get)
// 省略反序列的代码
for _, service := range services {
metricsOptions.ServiceReplicasCounter.
WithLabelValues(service.Name).
Set(float64(service.Replicas))
}
break
case <-quit:
return
}
}
}()
}UI
UI的代码很简单,主要就是一些前端的代码,调用上面的讲的一些API接口即可,这里就略去不表。
总结
Gateway是OpenFaaS最为重要的一个组件。回过头看整个项目的结构,Gateway就是一个rest转发服务,一个一个的handler,每个模块之间的耦合性不是很高,可以很容易的去拆卸,自定义实现相应的模块。
Night 17 2018 09 20 Grpcp (`content/night/17-2018-09-20-grpcp.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 17 期 grpc 开发及 grpcp 的源码分析 date: 2018-09-20T11:36:54+08:00 author: 林益帆
观看视频
{{< youtube id="sMBgYYEgm3c" >}}
Night 18 2018 09 27 CovenantSQL DH RPC (`content/night/18-2018-09-27-CovenantSQL-DH-RPC.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 18 期 去中心化加密通信框架 CovenantSQL/DH-RPC的设计 date: 2018-09-27T11:19:54+08:00 author: 王鹏程
观看视频
{{< youtube id="bAfiKsLbDeE" >}}
Night 19 2018 11 08 Http Router In Go (`content/night/19-2018-11-08-http-router-in-go.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 19 期 如何开发一个简单高性能的http router及gorouter源码分析 date: 2018-11-08T11:49:10+08:00 author: 徐佳军
观看视频
{{< youtube id="3BoStxKECL0" >}}
Night 2 2018 04 11 Teleport (`content/night/2-2018-04-11-teleport.md`)
title: 第 2 期 2018-04-11 线下分享内容 date: 2018-04-11T11:49:10+08:00
参与人数: 9 人
微服务相关的开源项目,直接根据 Github 讲解,三个相关的项目链接如下:
语音实录
以下内容是分享活动的实录回顾。
- 参与人数: 9人
- 参与者的自我介绍
- 演示 xiaoneai/ants
- 分析 tp-micro 和 teleport 框架代码
- 答疑
- 确定线下分享形式(标准包和开源项目)
Night 20 2018 11 15 Go Test (`content/night/20-2018-11-15-go-test.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 20 期 go test 及测试覆盖率 date: 2018-11-15T11:19:26+08:00 author: mai
观看视频
{{< youtube id="qkFFIIaTgHM" >}}
Night 21 2018 11 28 Errors In Go (`content/night/21-2018-11-28-errors-in-go.md`)
desc: 由 genpost (https://github.com/hidevopsio/genpost) 代码生成器生成 title: 第 21 期 Go errors 处理及 zap 源码分析 date: 2018-11-28T11:13:59+08:00 author: 叶飞/阙坦
观看视频
{{< youtube id="ImJim15N_Wc" >}}
Night 22 2018 12 06 Go Ide Discuss (`content/night/22-2018-12-06-go-ide-discuss.md`)
desc: VSCode Goland Sublime Text 3 Vim title: 第 22 期 Go 开发工具讨论 date: 2018-12-06T20:30:00+08:00 author: 杨文/John/...
观看视频
{{< youtube id="XjAQRnJj6zI" >}}
Night 23 2018 12 13 Drone Guide (`content/night/23-2018-12-13-drone-guide.md`)
desc: Drone 简单介绍和部分源码分析 title: 第 23 期 Drone 简单介绍和部分源码分析 date: 2018-12-13T20:35:00+08:00 author: 杨文
观看视频
{{< youtube id="Vg5EvcStD4k" >}}
参考资料
Night 24 2018 12 23 Go Mod Part 1 (`content/night/24-2018-12-23-go-mod-part-1.md`)
desc: go mod 源码阅读 title: 第 24 期 go mod 源码阅读 part 1 date: 2018-12-23T21:05:00+08:00 author: 杨文
Go 标准包阅读
Go 版本:go 1.11.5
观看视频
{{< youtube id="_Kdud_EN-eQ" >}}
阅读重点
- os.Stat
- filepath.SplitList
- os.Getwd()
- switch
- sync.Once
- os.IsNotExist(errMod)
- MustQuote
- AutoQuote
- modcmd.runGraph
format := func(m module.Version) string {
if m.Version == "" {
return m.Path
}
return m.Path + "@" + m.Version
}- sort.Slice
什么是 go mod
module 是相关 Go 依赖包的集合。module 是源代码交换和版本控制的单元。go 工具链会直接支持使用 go module,其功能包含记录和解析对其他第三方包的依赖项。模块将会替换旧的基于 $GOPATH 的模式
目前 Go1.11 是初步支持,后续建议持续观望一下。详见 godoc
开关
由于当前还在试验阶段,需要设置环境变量 GO111MODULE=on,才能够使用 go mod。支持一下选项:
- off:禁用 go module,按原有 $GOPATH、vendor 的寻址逻辑
- on:启用 go module
- auto:若当前不在 $GOPATH 下,且当前目录的根目录下含有 go.mod 文件。则启用 go module
go mod 一下
$ go mod
Go mod provides access to operations on modules.
Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.
Usage:
go mod <command> [arguments]
The commands are:
download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory
tidy add missing and remove unused modules
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed
Use "go help mod <command>" for more information about a command.- download:将 modules 下载到本地缓存
- edit:对 go.mod 进行编辑。具体可参见
go help mod edit - init:初始化 go module
- tidy:检索代码,新增缺少的依赖,删除不需要的依赖
- vendor:拷贝依赖,生成 vendor 目录
- verify:验证依赖是否正确
- why:解释为什么需要依赖和 modules
怎么找源码
在这里我们用最粗暴也是最简洁的办法,直接搜一下。在这里,我们找到了如下苗头:
- cmd/go/alldocs.go:go cmd 的文档。是通过
mkalldocs.sh在其他文件中收集注解生成的 godoc - cmd/go/internal/modcmd/mod.go:今天的男主角,go module 的代码就存储在
modcmd目录下。而其实我们搜索到的mod.go就是go mod这个命令的启动文件
看一看源码
modcmd
├── download.go
├── edit.go
├── graph.go
├── init.go
├── mod.go
├── tidy.go
├── vendor.go
├── verify.go
└── why.go通过 modcmd 的文件结构,可以惊喜地发现与 go mod <command> 的指令集其实是一致的。那么阅读的方向就很清晰了,我们可以按逻辑顺序看下去
init.go
package modcmd
import (
"cmd/go/internal/base"
"cmd/go/internal/modload"
"os"
)
var cmdInit = &base.Command{
UsageLine: "go mod init [module]",
Short: "initialize new module in current directory",
Long: `Init initializes and writes a new go.mod to the current directory...`,
Run: runInit,
}
func runInit(cmd *base.Command, args []string) {
modload.CmdModInit = true
if len(args) > 1 {
base.Fatalf("go mod init: too many arguments")
}
if len(args) == 1 {
modload.CmdModModule = args[0]
}
if _, err := os.Stat("go.mod"); err == nil {
base.Fatalf("go mod init: go.mod already exists")
}
modload.InitMod() // does all the hard work
}cmdInit
cmdInit 实际为定义 cmd 命令的基础结构体,其包含成员变量如下:
- UsageLine:用法
- Short:简短描述
- Long:详细描述
- Run:运行命令
其对应的触发场景主要是 help 和执行命令时,如下:
➜ ~ go help mod init
usage: go mod init [module]
Init initializes and writes a new go.mod to the current directory,
in effect creating a new module rooted at the current directory.
The file go.mod must not already exist.
If possible, init will guess the module path from import comments
(see 'go help importpath') or from version control configuration.
To override this guess, supply the module path as an argument.runInit
- 声明正在执行
go mod init命令集 - 判断参数是否不合法
- 参数合法下,该入参赋值给
go mod init的 module 参数(将args[0]赋予CmdModModule) - 判断是否存在 go.mod 文件(也就是判断是否已经初始化过)
- 在
modload.InitMod()中正式进行初始化的所有工作项
modload.InitMod()
源码中用 “does all the hard work” 来评价这个方法,它是 go mod init 的核心处理逻辑。接下来一起来看看 完整代码,我们将分为两个部分去阅读,如下:
一、MustInit
func MustInit() {
if Init(); ModRoot == "" {
die()
}
if c := cache.Default(); c == nil {
base.Fatalf("go: cannot use modules with build cache disabled")
}
}在该方法中,我们先进行必要的初始化,再读取构建缓存(不是本文重点),接下来详细阅读一下 Init 方法,如下:
func Init() {
...
env := os.Getenv("GO111MODULE")
switch env {
default:
base.Fatalf("go: unknown environment setting GO111MODULE=%s", env)
case "", "auto":
// leave MustUseModules alone
case "on":
MustUseModules = true
case "off":
if !MustUseModules {
return
}
}
if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
os.Setenv("GIT_TERMINAL_PROMPT", "0")
}
if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
}
var err error
cwd, err = os.Getwd()
if err != nil {
base.Fatalf("go: %v", err)
}
inGOPATH = false
for _, gopath := range filepath.SplitList(cfg.BuildContext.GOPATH) {
if gopath == "" {
continue
}
if search.InDir(cwd, filepath.Join(gopath, "src")) != "" {
inGOPATH = true
break
}
}
if inGOPATH && !MustUseModules {
if root, _ := FindModuleRoot(cwd, "", false); root != "" {
cfg.GoModInGOPATH = filepath.Join(root, "go.mod")
}
return
}
if CmdModInit {
ModRoot = cwd
} else {
...
if search.InDir(ModRoot, os.TempDir()) == "." {
ModRoot = ""
fmt.Fprintf(os.Stderr, "go: warning: ignoring go.mod in system temp root %v\n", os.TempDir())
return
}
}
...
search.SetModRoot(ModRoot)
}- 判断环境变量
GO111MODULE选项,主要是设置是否支持 go.mod 和处理一些异常 - 判断环境变量
GIT_TERMINAL_PROMPT选项,主要是涉及 Git 的密码弹窗输出提示的处理 - 判断环境变量
GIT_SSH选项,主要是判断 Git SSH 连接池,默认为禁用 - 判断当前路径是否在 $GOPATH 下(可以注意
filepath.SplitList相关联的代码。主要是读取了 $GOPATH 后利用特定标志位:进行了分隔,解决多 $GOPATH 的问题) - 判断当前是否在 $GOPATH 下且没有打开
GO111MODULE选项。若是则检索当前根目录下是否包含go.mod文件,存在则代表当前 $GOPATH 下存在 go.mod 文件,这里相对应的是auto选项时的逻辑 - 若当前
CmdModInit为启用,则在当前目录下创建 go.mod 文件,否则将尽量尝试去临时目录寻找标志文件
当 modRoot 为空时,则触发异常处理,常见的一些错误提示如下:
func die() {
if os.Getenv("GO111MODULE") == "off" {
base.Fatalf("go: modules disabled by GO111MODULE=off; see 'go help modules'")
}
if inGOPATH && !MustUseModules {
base.Fatalf("go: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'")
}
base.Fatalf("go: cannot find main module; see 'go help modules'")
}二、具体实现逻辑
/* Detailed source-code truncated for AI context efficiency. */- 判断是否已存在 go.mod 的文件句柄(代指已经处理过相应的逻辑)
- 检查 $GOPATH 是否设置,并判断 go.mod 文件是否已存在(代指是否已经初始化过)
- 若 oldSrcMod(
src/mod)存在,则 pkgMod(pkg/mod) 不存在,则进行重命名。这里考虑为兼容性操作 - 若为初次初始化,则执行以下步骤
- 第一步先做兼容处理,也就是执行
legacyModInit()对前身(vgo)的一些东西进行兼容处理转换为 go module 现在的模式 - 第二步执行
modFileToBuildList()方法 从modFile中初始化 mod 构建列表 - 最后通过
WriteGoMod进行逻辑处理后(例:处理最小依赖版本)将当前构建列表反写回 go.mod 文件
- 第一步先做兼容处理,也就是执行
- 若并非初次初始化,将会读取 go.mod 文件,根据语法解析 go.mod 的文件内容。接下来会进行一些基准操作
- 若 go.mod 文件是否为空,则先通过
FindModulePath检索现有的路径。另外在这里也做了godeps、govendor的兼容处理。寻找到 module path 后通过AddModuleStmt()添加 module path 到文件中 - 若只存在 module path,则通过
legacyModInit()进行兼容处理 - 最后与上小点一致,均为构建回写等动作
- 若 go.mod 文件是否为空,则先通过
总结
在 go module 中,更多的是本身对包管理工具的思考和实现。如果你仔细阅读过,可以想想如下方面:
- 为什么要这么做
- 为什么要在这个地方做
- 有没有更好的方法
依赖包管理工具,是 Go 一个比较要命的痛点。那为什么 go module 又能 "解决" 呢?请想想...
基于篇幅我没有把所有内容都写出来,但是写法、思维是类似的。有兴趣的同学可以认真看看视频,举一反三
问题
Go 1.11 在 go mod edit -module a/new/mod/name 命令中的一个 buggo mod edit 命令的 -module flag 是用于修改当前 module 的 path。也就是 go.mod 文件中,module 那一行。
在这个命令的源码 src/cmd/go/internal/modcmd/edit.go 文件 177 行开始:
if *editModule != "" {
modFile.AddModuleStmt(modload.CmdModModule)
}AddModuleStmt 这个函数的参数应该是 *editModule,而不是 modload.CmdModule。modload.CmdModule 只在 go mod init 命令启动时初始化。
// src/cmd/go/internal/modcmd/init.go
func runInit(cmd *base.Command, args []string) {
modload.CmdModInit = true
if len(args) > 1 {
base.Fatalf("go mod init: too many arguments")
}
if len(args) == 1 {
modload.CmdModModule = args[0] // INITIALIZATION IS HERE!
}
if _, err := os.Stat("go.mod"); err == nil {
base.Fatalf("go mod init: go.mod already exists")
}
modload.InitMod() // does all the hard work
}因此,由于 string 类型变量的 empty value 是空字符串,所以每次使运行 go mod edit -module a/new/module/name 并不会把 module path 修改为 a/new/module/name,而是修改为空字符串。
$ go mod init github.com/ziyi-yan/hello
go: creating new go.mod: module github.com/ziyi-yan/hello
$ cat go.mod
module github.com/ziyi-yan/hello
$ go mod edit -module github.com/ziyi-yan/hello-new
$ cat go.mod
module ""go 语言最新的代码 已经修复了这个 bug,预计在 Go 1.12 中发布。
Night 25 2018 12 27 Tsdb (`content/night/25-2018-12-27-tsdb.md`)
desc: TSDB 引擎介绍,对比及存储细节 title: 第 25 期 TSDB 引擎介绍,对比及存储细节 date: 2018-12-27T20:35:00+08:00 author: yuyang
TSDB 引擎介绍,对比及存储细节
- OpenTSDB
- InfluxDB
- Druid
观看视频
{{< youtube id="W-8PiciSWRM" >}}
Night 26 2019 01 03 Blog With Github Netlify (`content/night/26-2019-01-03-blog-with-github-netlify.md`)
desc: 手把手教你基于 Github+Netlify 构建自动化持续集成的技术团队博客 title: 第 26 期 Go 夜读之手把手教你基于 Github+Netlify 构建自动化持续集成的技术团队博客 date: 2019-01-03T21:15:00+08:00 author: John
Github
Netlify
观看视频
{{< youtube id="we6qrILQRjY" >}}
Night 27 2019 01 10 Go Mod Part 2 (`content/night/27-2019-01-10-go-mod-part-2.md`)
desc: go mod 源码阅读 part 2 title: 第 27 期 Go 夜读之 go mod 源码阅读 part 2 date: 2019-01-10T21:05:00+08:00 author: mai
Go 标准包阅读
Go 版本:go 1.11.5
学到的内容
1. mf := new(modfile.File)
2. lineno++ 感觉是无用的代码?
dep.go 中 ParseGopkgLock 方法第48行有用到 lineno ,会打印出 strconv.Unquote 解析错误的文件名和行号
if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' {
q, err := strconv.Unquote(val) // Go unquoting, but close enough for now
if err != nil {
return nil, fmt.Errorf("%s:%d: invalid quoted string: %v", file, lineno, err)
}
val = q
}其他几个文件可能是为了保持一致,或者为了将来输出错误信息特意保留的。其他文件里面的 lineno 没有地方引用,大家在阅读代码时会产生困惑,建议用空白符_来替代。
3. 判断外部网络是否可用
if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
t.Skipf("skipping test: no external network on %s", runtime.GOOS)
}runtime.GOOS返回程序所在的操作系统名
src\runtime\extern.go
// GOOS is the running program's operating system target:
// one of darwin, freebsd, linux, and so on.
const GOOS string = sys.GOOSNative Client(NACL) 是一种允许在浏览器中运行 native compiled code 的技术,允许开发者运用自己熟悉的语言来开发web应用,而不只是JavaScript,目前 NativeClient 技术只能应用于google自己的chrome中。
js 是指 Webassembly 技术,是在新版本1.11中才支持的,最新版本的浏览器可以支持。 NACL 和 JS 都不是真正的操作系统,不提供外部网络功能。
4.旧版模块管理配置转换为modfile
读取当前目录下旧版模块管理配置文件,从 Converters 中根据配置文件名去获取转换方法。
src\cmd\go\internal\modconv\convert.go
// ConvertLegacyConfig converts legacy config to modfile.
// The file argument is slash-delimited.
func ConvertLegacyConfig(f *modfile.File, file string, data []byte) error {
i := strings.LastIndex(file, "/")
j := -2
if i >= 0 {
j = strings.LastIndex(file[:i], "/")
}
convert := Converters[file[i+1:]]
if convert == nil && j != -2 {
convert = Converters[file[j+1:]]
}
if convert == nil {
return fmt.Errorf("unknown legacy config file %s", file)
}
mf, err := convert(file, data)
if err != nil {
return fmt.Errorf("parsing %s: %v", file, err)
}
...
}src\cmd\go\internal\modconv\modconv.go
var Converters = map[string]func(string, []byte) (*modfile.File, error){
"GLOCKFILE": ParseGLOCKFILE,
"Godeps/Godeps.json": ParseGodepsJSON,
"Gopkg.lock": ParseGopkgLock,
"dependencies.tsv": ParseDependenciesTSV,
"glide.lock": ParseGlideLock,
"vendor.conf": ParseVendorConf,
"vendor.yml": ParseVendorYML,
"vendor/manifest": ParseVendorManifest,
"vendor/vendor.json": ParseVendorJSON,
}目前go语言是使用map对象来存储旧配置文件和方法的映射关系这种设计思路的。@mai提出还有一种设计思路是抽象一个接口,各种配置管理来实现该接口。
观看视频
{{< youtube id="Isd-FOmM9C8" >}}
参考
1.Google Native Client
2.NaCl and networking
3.Go 1.11 正式发布 支持模块和WebAssembly
4.WebAssembly 现状与实战
Night 28 2019 01 17 Go Mod Part 3 (`content/night/28-2019-01-17-go-mod-part-3.md`)
desc: go mod 源码阅读 part 3 title: 第 28 期 Go 夜读之 go mod 源码阅读 part 3 date: 2019-01-17T20:05:00+08:00 author: mai
Go 标准包阅读
Go 版本:go 1.11.5
学到的内容
json:",omitempty"
方法一:
if path[len(path)-1] == '/' {
return fmt.Errorf("trailing slash")
}方法二:
strings.HasSuffix(path, "/")benchmark
方法一:
strings.TrimSuffix(pathMajor, "-unstable")方法二:
i := len(path)
if strings.HasSuffix(path, "-unstable") {
i -= len("-unstable")
}benchmark
if i := strings.Index(arg, "@"); i >= 0 {
path, vers = arg[:i], arg[i+1:]
}也可以用 split(arg, "@") 来实现。
观看视频
{{< youtube id="tD7Aj6tKhGc" >}}
Night 29 2019 01 23 Opentracing Jaeger In Go (`content/night/29-2019-01-23-opentracing-jaeger-in-go.md`)
desc: Go opentracing jaeger 集成及源码分析 title: 第 29 期 Go opentracing jaeger 集成及源码分析 date: 2019-01-23T21:00:00+08:00 author: jukylin
Go opentracing jaeger 集成及源码分析
一、分布式追踪论文
论文地址:http://bigbully.github.io/Dapper-translation/
为什么要用分布式追踪
当代的互联网的服务,通常都是用复杂的、大规模分布式集群来实现的。
互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、
可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。
因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具。
分布式系统调用过程
使用分布式追踪要留意哪些问题
低损耗
跟踪系统对在线服务的影响应该做到足够小。
应用透明
对于应用的程序员来说,是不需要知道有跟踪系统这回事的。
二、Opentracing简介
Opentracing的作用
OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
可以很自由的在不同的分布式追
踪系统中切换不负责具体实现
Opentracing主要组成
一个Trace
一个trace代表了一个事务或者流程在(分布式)系统中的执行过程
Span
记录Trace在执行过程中的信息
无限极分类
服务与服务之间使用无限极分类的方式,通过HTTP头部或者请求地址传输到最低层,从而把整个调用链串起来。
Jaeger-client的实现
Jaeger-client源码
提取
- 为什么要提取
主要作用是为了找到父亲
从哪里提取
进程内,不同进程之间各自约定
粟子:github.com/opentracing-contrib/go-stdlib/nethttp/server.go P86提取什么
texttraceid:spanid:parentid:是否采集uber-trace-id=157b74261b51d917:157b74261b51d917:0:1
github.com/jaegertracing/jaeger-client-go/propagation.go P124
注入
为什么要注入
主要为了让孩子能找到爸爸
注入到哪里
和提取相对
github.com/jaegertracing/jaeger-client-go/propagation_test.go注入了什么
github.com/jaegertracing/jaeger-client-go/propagation.go P103
异步report
Span.finish
github.com/jaegertracing/jaeger-client-go/span.go P177
把Span放入队列
github.com/jaegertracing/jaeger-client-go/reporter.go P219
从队列取出,生成thrift,放入spanBuffer
github.com/jaegertracing/jaeger-client-go/reporter.go P253
Flush到远程
github.com/jaegertracing/jaeger-client-go/transport_udp.go P113
低消耗
消耗在哪里
Jaeger-client作用于应用层,提取、注入、生成span、序列化成Thrift、发送到远程等,一系列操作这些都会带来性能上的损耗。
如何处理
选择合适采集策略:
- Constant
- Probabilistic
- Rate Limiting
- Remote
应用透明
- 如何做到让业务开发人员无感知
- Golang:
约定第一个参数为ctx,把parentSpan放入ctx
github.com/opentracing/opentracing-go/gocontext.go - PHP:
使用全局变量
- Golang:
三、Jaeger服务端源码阅读
服务端组件职责
各组件按照微服务架构风格设计,职责单一
Jaeger-agent负责上报数据的整理
Jaeger-collector负责数据保存
Jaeger-query负责数据查询
Jaeger-agent和Jaeger-collector使用基于TCP协议实现的RPC进行通讯
Jaeger-agent 源码阅读
监听3个UDP端口
github.com/jaegertracing/jaeger/cmd/agent/app/flags.go P35
github.com/jaegertracing/jaeger/cmd/agent/app/servers/thriftudp/transport.go P73接收Jaeger-client的数据,放入队列dataChan
github.com/jaegertracing/jaeger/cmd/agent/app/servers/tbuffered_server.go #80
从队列dataChan获取数据,进行校验
github.com/jaegertracing/jaeger/cmd/agent/app/processors/thrift_processor.go P108
提交数据
github.com/jaegertracing/jaeger/thrift-gen/jaeger/tchan-jaeger.go #39
Jaeger-collector 源码阅读
协程池
github.com/jaegertracing/jaeger/pkg/queue/bounded_queue.go
接收jaeger-agent数据
github.com/jaegertracing/jaeger/cmd/collector/app/span_handler.go P69
放入队列
github.com/jaegertracing/jaeger/cmd/collector/app/span_processor.go P112
从队列拿出来,写入数据库
github.com/jaegertracing/jaeger/cmd/collector/app/span_processor.go p54
github.com/jaegertracing/jaeger/plugin/storage/cassandra/spanstore/writer.go P136
四、Jaeger使用经验
监听指标
Jaeger-client 监听 reporter_spans
Jaeger-agent 监听 thrift.udp.server.packets.dropped
Jaeger-collector 监听 spans.dropped
http://localhost:16686/metrics
测试环境debug
测试环境记录执行mysql语句,redis命令,RPC参数、结果
可以很方便定位问题
性能调优
观察Jaeger-ui,对线上接口,mysql执行时间进行监控调优
观看视频
{{< youtube id="ub7jtN13KHA" >}}
Night 3 2018 04 18 Strings Part1 (`content/night/3-2018-04-18-strings-part1.md`)
title: 第 3 期 2018-04-18 线下活动 date: 2018-04-18T11:49:10+08:00
参与人数: 14 人
Go 标准包阅读
Go 版本:go 1.10.1
strings
- builder.go
- builder_test.go
- compare.go
- compare_test.go
- example_test.go
- export_test.go
- reader_test.go
- reader.go
问题清单
以下是我们在阅读过程中的一些问题,希望可以引起大家的关注,也欢迎大家提出自己的理解,最好可以给以文章总结。
- defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
- runtime.ReadMemStats(&m1)
- defer_lock.go
package main
import (
"sync"
)
func main() {
var mu sync.Locker = new(I)
defer LockUnlock(mu)()
println("doing")
}
func LockUnlock(mu sync.Locker) (unlock func()) {
mu.Lock()
return mu.Unlock
}
type I struct{}
func (i *I) Lock() {
println("lock")
}
func (i *I) Unlock() {
println("unlock")
}buf := make([]byte, len(b.buf), 2*cap(b.buf)+n) 为什么是2倍呢?
// NOTE(rsc): This function does NOT call the runtime cmpstring function,
// because we do not want to provide any performance justification for
// using strings.Compare. Basically no one should use strings.Compare.
// As the comment above says, it is here only for symmetry with package bytes.
// If performance is important, the compiler should be changed to recognize
// the pattern so that all code doing three-way comparisons, not just code
// using strings.Compare, can benefit.b.buf = append(b.buf, s...) s是string,b.buf是[]byte
int int64的问题? 在32位机器上进行int64原子操作时的panic
defer LockUnlock(mu),如果LockUnlock(mu)没有带(),则会丢失func函数的执行
if r < utf8.RuneSelf
- text
if cap(b.buf)-l < utf8.UTFMax { Example
xxx_test.go 其实是xxx包,但是又不想放到xxx包里面,因为它只是提供给_test.go包使用的函数。
rune 码点的处理(reader.go prevRune int // index of previous rune; or < 0)
off 与 offset 尴尬的问题
whence where when ?wiki-whence
// It is similar to bytes.NewBufferString but more efficient and read-only.
Go 1.10 开始引入了 builder
# https://golang.org/doc/go1.10#strings
strings ¶
A new type Builder is a replacement for bytes.Buffer for the use case of accumulating text into a string result. The Builder's API is a restricted subset of bytes.Buffer's that allows it to safely avoid making a duplicate copy of the data during the String method.延伸阅读
Night 30 2019 02 16 Go Mod Part 4 (`content/night/30-2019-02-16-go-mod-part-4.md`)
desc: Go 夜读之 go mod 源码阅读 part 4 title: 第 30 期 Go 夜读之 go mod 源码阅读 part 4 date: 2019-02-16T21:00:00+08:00 author: mai
Go 标准包阅读
Go 版本:go 1.11.5
net/http
- `` 换行
本期没有视频回放。
观看视频
{{< youtube id="" >}}
Night 31 2019 02 23 Flag (`content/night/31-2019-02-23-flag.md`)
desc: Go 夜读之 flag 包源码阅读 title: 第 31 期 Go 夜读之 flag 包源码阅读 date: 2019-02-23T21:00:00+08:00 author: mai
Go 标准包阅读
Go 版本:go 1.11.5
总结
- *v.URL = *u
- flag 下有
package flag_test?? - init 中定义相同的 stringvar ;
当一个文件中出现多个 init 函数时,他们都会被加载,并且以 init 出现在文件中的前后顺序执行。
type Value interface {
String() string
Set(string) error
}
type Getter interface {
Value
Get() interface{}
}
type boolFlag interface {
Value
IsBoolFlag() bool
}strconv.ParseBool的返回值可以被利用
v, err := strconv.ParseBool(s)
*b = boolValue(v)
return err参考资料
观看视频
{{< youtube id="z-9WEuUWqu4" >}}
Night 32 2019 03 02 Etcd Raft (`content/night/32-2019-03-02-etcd-raft.md`)
desc: Go 夜读之 etcd raft 源码阅读 title: 第 32 期 Go 夜读之 etcd raft 源码阅读 date: 2019-03-02T21:05:00+08:00 author: 缪昌新
etcd raft 阅读
etcd 版本:3.3.10
2019.3.2 晚上
总结
etcd里的raft模块只实现了raft共识算法,而像消息的网络传输,数据存储都由上层应用来完成。
下面是各个文件(夹)的功能简介:
- raftpb
用Protocol Buffer定义了一些需要序列化的数据结构,比如Entry和Message。
- log_unstable.go
unstable数据结构表示用于还没有被用户层持久化的数据,它维护了两部分内容snapshot和entries。
- storage.go
这个文件定义了一个Storage接口,应用层需要实现这个接口,以提供存储和查询日志的能力。
- log.go
维护本地日志信息。其中的committed和applied分别表示已提交和已经应用到状态机的日志索引。
- progress.go
Leader节点通过Progress这个数据结构来追踪一个follower的状态,并根据Progress里的信息来决定每次同步的日志项。
- raft.go
Raft协议的具体实现就在这个文件里。其中最重要的就是Step函数,它用来处理不同的消息。所以以后当我们想知道raft对某种消息的处理逻辑时,到这里找就对了。
- node.go
node的主要作用是应用层和共识模块(raft)的衔接。将应用层的消息传递给底层共识模块,并将底层共识模块共识后的结果反馈给应用层。
参考资料
- 深入浅出 Raft - 基本概念
- 深入浅出 Raft - Membership Change
- 深入浅出 Raft - Leader 选举
- 深入浅出 Raft - Optimization
- Raft在etcd中的实现
- etcd contrib
- Etcd超全解:原理阐释及部署设置的最佳实践
观看视频
{{< youtube id="sL02PsR20gE" >}}
Night 33 2019 03 07 Defer In Go (`content/night/33-2019-03-07-defer-in-go.md`)
desc: Go 夜读之 Go defer 和逃逸分析 title: 第 33 期 Go 夜读之 Go defer 和逃逸分析 date: 2019-03-07T21:05:00+08:00 author: 饶全成
2019.3.7 晚上
观看视频
{{< youtube id="-FtBBx44E3g" >}}
Night 34 2019 03 16 Plan9 Guide (`content/night/34-2019-03-16-plan9-guide.md`)
desc: Go 夜读之 plan9 汇编入门,带你打通应用和底层 by Xargin title: 第 34 期 Go 夜读之 plan9 汇编入门,带你打通应用和底层 by Xargin date: 2019-03-16T21:05:00+08:00 author: Xargin
2019.3.16 晚上 21 点 ~ 23点
直播过程中的文字讨论 (如有涉及到隐私,请告知)
/* Detailed source-code truncated for AI context efficiency. */观看视频
{{< youtube id="dPdXxex1v_4" >}}
Night 35 2019 03 21 Reading Context (`content/night/35-2019-03-21-reading-context.md`)
desc: Go 夜读之 context 源码阅读 title: 第 35 期 context 源码阅读 date: 2019-03-21T21:05:00+08:00 author: mai
预习材料
第 35 期 Go 夜读之『context 包源码阅读』预习资料 #191
观看视频
{{< youtube id="LhEiCTkF2S8" >}}
Night 36 2019 03 28 Reading K8s Context (`content/night/36-2019-03-28-reading-k8s-context.md`)
desc: Go 夜读之 k8s context 实践源码阅读 title: 第 36 期 k8s context 实践源码阅读 date: 2019-03-28T21:05:00+08:00 author: mai
实践
- WithValue
- WithCancel
- WithTimeout
WithDeadline 基本上没有用到。
参考资料
- How to correctly use context.Context in Go 1.7
- How to correctly use package context
- 视频笔记:如何正确使用 Context - Jack Lindamood
观看视频
{{< youtube id="JLuBPEeS9G8" >}}
Night 37 2019 04 01 Talk From Serverless In Apache Pulsar (`content/night/37-2019-04-01-talk-from-serverless-in-apache-pulsar.md`)
desc: Go 夜读之 从 serverless 的一个设计说起 title: 第 37 期 从 serverless 的一个设计说起 date: 2019-04-01T21:05:00+08:00 author: 冉小龙
参考资料
观看视频
{{< youtube id="wfTQlM8eics" >}}
Night 38 2019 04 13 K8s Scheduler Reading (`content/night/38-2019-04-13-k8s-scheduler-reading.md`)
desc: Go 夜读之 kubernetes scheduler 源码阅读 title: 第 38 期 kubernetes scheduler 源码阅读 date: 2019-04-13T21:05:00+08:00 author: John
观看视频
{{< youtube id="T83rJuIs7PE" >}}
Night 39 2019 04 18 Init Function In Go (`content/night/39-2019-04-18-init-function-in-go.md`)
desc: Go 夜读之 init function 使用分析 title: 第 39 期 init function 使用分析 date: 2019-04-18T21:05:00+08:00 author: mai
观看视频
{{< youtube id="62mci2mPQDU" >}}
Night 4 2018 04 25 Strings Part2 (`content/night/4-2018-04-25-strings-part2.md`)
title: 第 4 期 2018-04-25 线下活动 date: 2018-04-25T11:49:10+08:00
参与人数: 15 人
Go 标准包阅读
Go 版本:go 1.10.1
strings
- replace.go
- replace_test.go
- search.go
- search_test.go
问题清单
以下是我们在阅读过程中的一些问题,希望可以引起大家的关注,也欢迎大家提出自己的理解,最好可以给以文章总结。
- Boyer-Moore 算法
延伸阅读
- Boyer-Moore_string_search_algorithmde
- Boyer-Moore字符串搜索算法
- 字符串匹配的Boyer-Moore算法
- grep之字符串搜索算法Boyer-Moore由浅入深(比KMP快3-5倍)
Night 40 2019 04 27 Atomic Value In Go (`content/night/40-2019-04-27-atomic-value-in-go.md`)
desc: Go 夜读之 atomic.Value 的使用和源码分析 title: 第 40 期 atomic.Value 的使用和源码分析 date: 2019-04-27T21:05:00+08:00 author: mai
观看视频
{{< youtube id="pyics4WyUmA" >}}
Night 41 2019 05 12 Golint Golangci Lint (`content/night/41-2019-05-12-golint-golangci-lint.md`)
desc: Go 夜读之 golint 及 golangci-lint 的介绍和使用 title: 第 41 期 golint 及 golangci-lint 的介绍和使用 date: 2019-05-12T20:00:00+08:00 author: mai
观看视频
{{< youtube id="z42y4tpRmbw" >}}
Night 42 2019 05 16 Go Failpoint Design (`content/night/42-2019-05-16-go-failpoint-design.md`)
desc: Go 夜读之 An Introduction to Failpoint Design title: 第 42 期 An Introduction to Failpoint Design date: 2019-05-16T21:00:00+08:00 author: 龙恒
观看视频
{{< youtube id="ke7zzny9dxU" >}}
Night 43 2019 05 23 Gomonkey Framework Design And Practives (`content/night/43-2019-05-23-gomonkey-framework-design-and-practives.md`)
desc: Go 夜读之 gomonkey 框架设计与应用实践 title: 第 43 期 gomonkey 框架设计与应用实践 date: 2019-05-23T21:30:00+08:00 author: 张晓龙
观看视频
{{< youtube id="OpuX47E7B2w" >}}
Night 44 2019 05 29 Go Map Reading (`content/night/44-2019-05-29-go-map-reading.md`)
desc: Go 夜读之 Go map 源码阅读分析(20190529第44期) title: 第 44 期 Go map 源码阅读分析 date: 2019-05-29T21:10:00+08:00 author: 饶全成
观看视频
{{< youtube id="P2v3kvztWU0" >}}
Night 45 2019 05 30 Goim Reading (`content/night/45-2019-05-30-goim-reading.md`)
desc: Go 夜读之 goim 架构设计与源码分析(20190530第45期) title: 第 45 期 goim 架构设计与源码分析 date: 2019-05-30T21:10:00+08:00 author: tsingson
观看视频
{{< youtube id="bFniRH3ifx8" >}}
Night 46 2019 06 05 Tidb Overview Reading (`content/night/46-2019-06-05-tidb-overview-reading.md`)
desc: Go 夜读 && TiDB 源码阅读之概览 title: 第 46 期 TiDB 源码阅读之概览 date: 2019-06-05T21:10:00+08:00 author: 龙恒
TiDB Source Code Overview
视频回看
意见反馈
chat 答疑
20:54:52 From mai yang : 大家好,欢迎大家前来参加 Go 夜读&TiDB 源码学习!
21:22:34 From nange : Session 怎么初始化的?
21:22:46 From ccong deng : 每个连接都是跟一个session对象对应么?
21:22:48 From jeffery : session主要包含什么?
21:22:59 From jeffery : 譬如:
21:23:01 From Wei Yao : 对,一个链接一个 session
21:23:09 From Wei Yao : 具体包含什么,可以大家自己去看了
21:23:17 From Wei Yao : 这个线上不可能所有都讲的
21:23:25 From jeffery : 好的,谢谢了
21:31:00 From Wei Yao : 大家如果对语法分析,词法分析感兴趣,可以去看看 yacc 跟 lex
21:31:10 From hezhiyong : parser 这一层不是使用mysql的parser吗
21:31:17 From Wei Yao : 不,我们自己写的
21:31:56 From hezhiyong : mysql 的语法解析是在那一步用到了?
21:32:01 From tianyi wang : select coalesce()中coalesce是在fields里面吗
21:32:10 From Wei Yao : 我们的语法解析就是兼容 mysql,
21:32:12 From [email protected] : SQL injection 有做嗎?
21:32:27 From Wei Yao : SQL injection?SQL 注入?
21:32:40 From Wei Yao : 我们不叫 sql 注入
21:32:55 From [email protected] : 恩?
21:33:04 From Wei Yao : 我们会把 sql 变成算子,之后会去优化算子结构,下面会讲,
21:33:15 From [email protected] : 好的,謝謝。
21:34:55 From jeffery : 刚刚的意思:Visitor是选择节点
21:34:58 From jeffery : ?
21:35:05 From Wei Yao : 不是
21:35:11 From Abner Zheng : 一种设计模式
21:35:13 From xietengjin : 遍历节点用的吧
21:35:14 From Wei Yao : visitor 是设计模式中的那个 visitor 模式
21:35:17 From Wei Yao : 对
21:35:19 From jacobz : 遍历树用的
21:35:23 From Fangfang Qi : 是遍历语法树的
21:35:27 From jeffery : 额,好的
21:35:28 From Wei Yao : 遍历 ast 树
21:37:03 From jeffery : 清楚
21:40:01 From jacobz : 是搞优化的那一堆?
21:43:45 From lk : 递归遍历?
21:44:05 From Wei Yao : 层级有限。
21:48:06 From Kathy : 其实这个时候是不是类似传统的通过运算符进栈出栈形成表达式
21:48:24 From Wei Yao : 对,表达式系统基本上都是这样
21:51:21 From Kathy : ScalarFunction能解决aggregation的函数的语句吗
21:55:24 From Chen Shuang : 能
21:55:55 From Chen Shuang : aggregation function 也是 scalar function.
21:57:23 From Kathy : 只要不涉及其他表的相关列的function是否都最后成为scalarFunction
21:57:33 From Kathy : 的表达式
21:58:40 From Chen Shuang : 只要是 function , 都会变成 scalarFunction 表达式
21:59:40 From Chen Shuang : select t1.a + t2.b from t1,t2; 其中 t1.a + t2.b 会build 成一个 scalarFunction 表达式
21:59:40 From Kathy : 多谢答复
22:00:28 From Chen Shuang : 不客气哈
22:06:26 From tianyi wang : select coalesce()也会是scalarfunction?
22:06:53 From hezhiyong : 可以演示一下debug一条语句跑的代码吗
22:10:07 From jiangchen : 是的,能不能最好演示下。。每次next返回的是一部分子结果还是一部分最终的结果?
22:11:33 From Kathy : 执行引擎的新特性可以说说吗?简单讲一下,就是parallel physical operator的实现等等
22:11:57 From Wei Yao : 执行引擎下周讲
22:12:05 From 慢摇哥哥 : 老师,Coprocessor是在哪一步分发的
22:12:06 From jeffery : 辛苦了,有一个基本的逻辑了
22:12:37 From Kathy : 好的 谢谢
22:12:42 From 达 黄 : 之前看了tidb源码解析的文章 配合着这个视频 印象更清楚了
22:13:32 From jeffery : 感觉姚老师像一位老教授在督导
22:14:02 From Wei Yao : :)
22:15:40 From jeffery : 为什么这部分会单独出来?
22:15:40 From nange : Distsql是什么好像没讲。
22:15:54 From tianyi wang : select coalesce()会是scalarfunction还是单独的一部分呢?
22:18:53 From 熊浪 : 问下是每一个session都会解析一次sql么?如果一个sql在同一个session中多次执行是否有ast的共享?
22:21:02 From hezhiyong : prepare 是要开启参数才可以的吧
22:21:36 From 熊浪 : 好的,和mysql是一样的。谢谢
PPT: https://talkgo.slack.com/files/U8A45L223/FKA335THT/_reading-go__tidb_source_cdoe_overview.pdf
观看视频
{{< youtube id="mK6BOquvQhE" >}}
Night 47 2019 06 12 Tidb Exector Reading (`content/night/47-2019-06-12-tidb-exector-reading.md`)
desc: Go 夜读 && TiDB 源码阅读之 Executor title: 第 47 期 TiDB 源码阅读之 Executor date: 2019-06-12T21:10:00+08:00 author: chenshuang
TiDB Executor 内容介绍
本次分享主要讲 TiDB 中 insert/update/delete/select, 以及 DDL 等是如何执行的,以及涉及到相关模块。大概会涉及以下模块:
- executor
- distsql
- ddl
推荐阅读
视频回看
PPT: https://github.com/talkgo/night/files/3281080/TiDB.Executor.pdf
问题
- 表的信息是怎么存的呢
- id的生成规则是什么
- 如果索引里面不保存handle_id,那怎么根据索引找到这行数据呢
- 索引字段很大会不会有问题,作为id的一部分的话
- 单条6m的限制是怎么计算出来的?还是压力测出来的?
- ddl时,job放到tikv的队列,tikv是分布式的,job具体是放到哪个tikv上的呢?
- 并行ddl 如何跑
- tikv整体上可以看成一个kv store
- region这部分概念可以配合hbase去看看能更好的理解
- 难道own tidb server要遍历所有的tikv server上的queue,去取ddl的job?
- tidb 的统计信息也是放一个表里面,每次parse 都会去拿这个信息,这样的话请求到一个region,这个表是不是很容易成为热点
观看视频
{{< youtube id="Rcrm4w7sqbM" >}}
Night 48 2019 06 19 Tidb Compiler Reading (`content/night/48-2019-06-19-tidb-compiler-reading.md`)
desc: Go 夜读 && TiDB 源码阅读之 Compiler title: 第 48 期 TiDB 源码阅读之 Compiler date: 2019-06-19T21:10:00+08:00 author: wangcong
TiDB Compiler 内容介绍
本次分享主要讲 TiDB 的优化器框架以及具体的 SQL 执行优化原理 。主要涉及 TiDB 的 planner 模块。欢迎大家参加!
PPT: TiDB Compiler.pdf
推荐阅读
视频回看
问题
22:13:46 From mai yang : rule 怎么对照文档帮助理解呢?
22:16:31 From dqyuan : 怎么快速找到代码对应的pr?
22:18:43 From Wei Yao : git blame
22:23:04 From kzl : Order by 是会下推到tikv吗?
22:25:02 From Wei Yao : 除非 order by 带了 limit,要不然推下去没意义
22:25:25 From Wei Yao : 有一些情况,如果是 order by 一个索引,那就直接消除掉这个 排序操作了
22:26:07 From zhao : 有意义吧,推了之后 tidb端可以直接stream merge,不知道实现了没有
22:27:38 From Wei Yao : 是可以 stream merge, 但是现在 tidb 还没实现这个,因为优先级不是太高
22:30:42 From Wei Yao : stream merge 主要是可以节省一些内存,避免 order by 太多导致 tidb oom
22:30:56 From Heng Long : 嗯,会让 tikv 的压力变大
22:34:28 From hezhiyong : limit offset 分页性能不好
22:34:53 From hezhiyong : 有好变通改写方法没
22:35:52 From hezhiyong : limit offset 会有下推到tikv么
22:36:19 From Wei Yao : limit offset 没办法的,这个是全局的 offset
22:36:40 From Wei Yao : tikv 并不知道自己的 offset 在全局的 offset 是多少
22:37:05 From Wei Yao : 这个其他数据库其实也一样
22:37:37 From hezhiyong : 那就是这个数据就是要全拿到tidb层在来过滤
22:37:38 From hezhiyong : 是吧
22:39:27 From Hao’s iPad : 喝口水吧
22:45:35 From zhao : 这个skyline prune有相关的资料吗
22:45:41 From zhao : paper之类的
22:47:09 From Wei Yao : 我记得暂时还没有 public
22:47:20 From Wei Yao : skyline pruning 就是消除一些路径
23:04:22 From mai yang : 怎么快速找到代码对应的pr?git blame
这个可以演示一下吗?
23:06:42 From tangenta : github 上面看文件的时候有个选项是 blame,那里应该比较清晰
23:09:44 From mai yang : github 上面看文件的时候有个选项是 blame,那里应该比较清晰
——
这个不错,看到了。
观看视频
{{< youtube id="4mgx8bq_fcQ" >}}
Night 49 2019 06 26 Tidb Transaction Reading (`content/night/49-2019-06-26-tidb-transaction-reading.md`)
desc: Go 夜读 && TiDB 源码阅读之 Transaction title: 第 49 期 TiDB 源码阅读之 Transaction date: 2019-06-26T21:10:00+08:00 author: zimulala
TiDB Transaction 内容介绍
本次分享主要讲 TiDB 的事务执行过程和一些异常处理,涉及 TiDB 的 session 和 tikv 部分模块。
PDF: Source code reading of TiDB Transaction .pdf
推荐阅读
视频回看
问题
21:17:34 From zq : 分享妹子用的是什么IDE
21:17:44 From mrj : goland
21:17:45 From Pure White : 左上角,goland
21:17:45 From tangyinpeng : goland
21:17:46 From Heng Long : goland
21:17:57 From zq : goland现在做得这么好看啦
21:18:05 From Heng Long : Meterial theme
21:18:07 From mrj : 下来主题
21:18:22 From lk : 有什么比较不错的主题吗?
21:18:31 From Pure White : darcula
21:18:35 From mrj : 默认的就挺好的
21:20:04 From mai yang : 明天晚上将由 GoLand 布道师给我们分享 GoLand 的使用及技巧实践分享。
21:28:23 From HAITAO的 iPhone : 点查不带timestamp,直接读最新稳定版本么?
21:28:32 From Wei Yao : 对
21:28:52 From liber xue : 双击shift 直接search
21:28:55 From Wei Yao : 最新 commited 版本
21:35:50 From HAITAO的 iPhone : 点查,实际会默认给一个当前最新的timestamp,根据这个ts,kv返回对应的版本值?还是不带任何ts,发给kv ?
21:36:29 From Wei Yao : 用 maxTs
21:50:04 From openinx : A very nice talk.
22:05:22 From kzl : 获取完成之后,region扩容了,数据迁移走了怎么办?
22:06:16 From jeff : 是说 region 分裂了吧。
22:06:33 From kzl : 对的
22:08:46 From ruiayLin : region信息就会过期
22:11:05 From jeff : 那提交的时候会重试吧
22:11:39 From hezhiyong : tidb不断缓存region 的信息会不会占用很大的内存
22:13:23 From jeff : 唔,这里应该只缓冲曾经用到的 region ,并不是集群中所有 region
22:13:52 From jeff : s/ 缓冲 / 缓存 /g
22:14:44 From jeff : 貌似讲到刚才数据 region 分裂后的场景了。
22:14:56 From Wei Yao : 会重试
22:26:54 From fj : 大神 tidb的事物隔离级别 能介绍下吗?- ̗̀(๑ᵔ⌔ᵔ๑)
22:29:35 From Tengjin Xie : snapshot isolation?
22:31:48 From Wei Yao : 比 mysql 的 rr 稍微高一点
22:33:41 From fj : 刚才 讲的tidb的隔离级别是?
22:34:03 From Wei Yao : 你可以认为是 可重复读
22:34:11 From Wei Yao : 其实这是快照隔离级别
观看视频
{{< youtube id="A46VE3aUTKo" >}}
Night 5 2018 05 10 Strings Part3 (`content/night/5-2018-05-10-strings-part3.md`)
title: 第 5 期 2018-05-10 线下活动 - Go 标准包阅读 date: 2018-05-10T11:49:10+08:00
参与人数: 20 人
Go 标准包阅读
Go 版本:go 1.10.1
strings
- strings.go(进度50%)
问题清单
以下是我们在阅读过程中的一些问题,希望可以引起大家的关注,也欢迎大家提出自己的理解,最好可以给以文章总结。
重头戏:Rabin-Karp search
Rabin-Karp 算法的思想:
- 假设待匹配字符串的长度为M,目标字符串的长度为N(N>M);
- 首先计算待匹配字符串的hash值,计算目标字符串前M个字符的hash值;
- 比较前面计算的两个hash值,比较次数N-M+1:
- 若hash值不相等,则继续计算目标字符串的下一个长度为M的字符子串的hash值
- 若hash值相同,则需要使用朴素算法再次判断是否为相同的字串
- 16777619 为什么是这个值? RK, FNV 算法
16777619 = (2^24 + 403)) - len 计算问题?是否是每次都会计算,直接拿值,不需要单独计算的;
len(string) 的获取 string 的长度问题:
涉及到 string 的结构问题。
在runtime/strings.go 中可以看到对应的 string 结构:
type stringStruct struct {
str unsafe.Pointer
len int
}可以得到在求 string 的长度的时候,实际上是直接获取值。
在 slice 的结构体中
type slice struct {
array unsafe.Pointer
len int
cap int
}Len 方法跟 len 长度走。
type hmap struct {
// Note: the format of the Hmap is encoded in ../../cmd/internal/gc/reflect.go and
// ../reflect/type.go. Don't change this structure without also changing that code!
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}在map的结构体重 有个 count 的统计 map 的内部数量。
- len 与 runtime 包里面的某些实现的有何区别?
func IndexByte(s string, c byte) int // ../runtime/asm_$GOARCH.s)
- strings.s
# strings.s
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file is here just to make the go tool happy.*.s 文件存在的原因是 Go 在编译的时候会启用 -compile 编译器 flag ,它要求所有的函数必须包含函数体,创建一个空的汇编语言文件绕开这个限制。
- go:linkname
控制谁可以调用它。
Go 的隐藏功能
//go:noescape
//go:noinline
//go:nosplit
//go:linkname
...
其它
// +build
//go:generate
package xxx // import "xxx"
//line其中有一些 net_linux.go 或 asm_amd64.s,Go 语言的构建工具将只在对应的平台编译这些文件。
如果在包中加入 // +build linux darwin 表示该包只在 linux 和 mac 下被编译。
而 // +build ignore 是忽略该包。
它跟 internal 有什么不同呢?
一个internal包只能被和internal目录有同一个父目录的包所导入。
举例说明:
time.Sleep()的实现函数在runtime包的time.go
...
其他更多的使用,大家可以自行搜索 go:linkname
更多相关知识,大家可点击:突破限制,访问其它Go package中的私有函数
- (i+16)/8 这个16,8是什么意思?怎么解读这个逻辑的呢?
// Switch to indexShortStr when IndexByte produces too many false positives.
// Too many means more that 1 error per 8 characters.
// Allow some errors in the beginning.
if fails > (i+16)/8 {
r := indexShortStr(s[i:], substr)
if r >= 0 {
return r + i
}
return -1
}- 逻辑是什么意思呢?
// contains reports whether c is inside the set.
func (as *asciiSet) contains(c byte) bool {
return (as[c>>5] & (1 << uint(c&31))) != 0
}- makeASCIISet
// ascii空格包括\t,\n,\v,\f,\r, ` `
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}延伸阅读
大家一定要看这一篇文章:Rabin-Karp 算法(字符串快速查找)- primes-16777619
- Fowler–Noll–Vo hash function
- FNV Hash
- FNV哈希算法【学习】
- 字符串查找算法(二)
- 突破限制,访问其它Go package中的私有函数
- How to call private functions (bind to hidden symbols) in GoLang
- 『深入解析 Go 之基本类型-字符串』
Night 50 2019 06 27 Goland Practice (`content/night/50-2019-06-27-goland-practice.md`)
desc: Go 夜读之 GoLand Tips & Tricks title: 第 50 期 Go 夜读之 GoLand Tips & Tricks date: 2019-06-27T21:10:00+08:00 author: Florin & Shengyou Fan
GoLand Tips & Tricks
问答
21:01:08 From Shengyou Fan : 全程英文+中文翻译
21:04:11 From Hao : Shengyou , r u come from TW ?
21:04:50 From Shengyou Fan : Yes, I’m from TW
21:10:04 From lucas : ctrl + tab 对应 mac 是?
21:10:12 From mai yang : presentation assistant
21:10:26 From Shengyou Fan : 也是 ctrl + tab
21:14:26 From Dominic : 这种 example 自己在document中定义的话 是代码块吗
21:27:37 From lucas : 有什么办法可以在terminal中快速打开一个文件吗?
21:27:46 From 江金 饶 to mai yang (Privately) : open xxx
21:27:50 From mai yang to 江金 饶 (Privately) : opne xxx
21:27:59 From mai yang : open xxx
21:28:01 From Dominic : open on mac or start on win
21:28:03 From Hao : Mac, open file
21:28:08 From Zhongxuan的 iPhone : Open .
21:28:16 From lucas : 在ide中打开
21:28:33 From Hao : .......
21:28:48 From mrj : 你把对应的文件关联到ide就行了
21:29:05 From lucas : ide中的terminal 应该是可以做到的
21:29:08 From lucas : 但是我没找到
21:29:47 From mai yang : 新版本 2019.1.3 好像不能 goland . 打开某个项目了,你们的可以吗?
21:32:57 From kevin : 双击shift还是挺不错的
21:33:04 From Kevin Bai : command + shift + A 👍
21:34:21 From mai yang : 有快捷键冲突也是很麻烦。。。
21:35:47 From mai yang : 这是全新的一种开发方式。
21:40:17 From kevin : 这个就牛逼了
21:40:24 From zhaohe : 6666
21:40:29 From bruce : 一直都用
21:40:34 From tangyinpeng : 我原来不会用goland
21:40:45 From bruce : 我一直在用这个功能
21:40:48 From kevin : 还可以自动生成
21:42:20 From Dominic : 以前玩android studio的时候模板代码玩的很溜 超级提升生产力 →_→
21:42:26 From mrj : 可视化编程
21:43:26 From mrj : ctrl+t
21:44:37 From kevin : 看来工具还是用的不够6啊
21:46:14 From Zhongxuan的 iPhone : 跟我用mac不太一样,有点难受
21:49:15 From tangyinpeng : 做笔记啊,兄弟们
21:49:42 From kevin : 看下今晚收获少
21:49:49 From kevin : 收获多少
21:50:17 From f430 w : 掌握快捷键效率杠杠的
21:53:47 From razil : alt+enter 大法好
21:55:34 From f430 w : 当然了ide快捷键用多了,不利于白板徒手写code,哈哈
21:55:51 From razil : 面试凉凉
21:56:23 From Kevin Bai : 想太多了,先听课吧
21:58:58 From tangyinpeng : 重构快捷键是哪个来着,对不住,刚刚分心了
21:59:15 From Dominic : ctrl alt shift T
21:59:19 From tangyinpeng : tk
21:59:22 From mai yang : ctrl+t
22:06:11 From bruce : 这个req太简单
22:06:15 From bruce : 用过。
22:06:45 From Kevin Bai : 下一步搞不好就有惊艳
22:07:03 From bruce : 嗯
22:08:28 From Dominic : 咦 单元测试可以用这个玩吗
22:12:58 From Kevin Bai : 最近刚知道这个share
22:16:40 From mai yang : Share 怎么用呢?
22:22:09 From Kevin Bai : 会在 .idea 下生成一个 run configuration ,然后可以把share的功能传到git上
22:33:38 From mrj : 这个吊
22:45:30 From 陳明進 : 直接run test with cpu profile真的是有點屌
22:53:29 From mai yang : 厉害!
22:54:13 From 陳明進 : 這跟chrome devTool學的吧....
22:56:38 From Dominic : thank u for this great presentation
22:56:53 From Kevin Bai : 多线程调试有什么建议 ?
22:58:14 From Eiger : 我想问下go项目包管理最推荐的是用哪个啊
22:58:26 From Dominic : go module
22:58:28 From yongping zhao : 肯定mod呀
22:58:52 From Eiger : 但是国内经常要手动获取库版本再replace
22:59:13 From Dominic : goproxy
23:00:00 From Eiger : 有免费的代理吗还是需要自己在国外搭?个人使用
23:00:22 From Dominic : goproxy.io 开源的
23:00:24 From yongping zhao : go proxy。。搜这个关键字。
23:01:26 From yongping zhao : 多线程调试有什么建议 ?
23:02:00 From mai yang : https://blog.jetbrains.com/cn
23:02:20 From mrj : ctrl+e
23:02:23 From mai yang : Double+shift
alter+enter
23:04:52 From Kevin Bai : 👍
23:05:19 From mai yang : Productivity Guide 这个很好。
23:05:44 From mrj : windwos 的 terminal 不太好用
23:06:04 From Dominic : 我一般替换成 git bash
23:06:12 From Lewis : +1
23:06:57 From Hao : hi florin , could you please show us the go mod difference between cmd and goland IDE?
23:07:58 From Kevin Bai : 是
23:07:59 From Kevin Bai : debug
23:09:12 From Dominic : 👍
23:10:57 From Lewis : double Ctrl 具体怎么用的?一直没搞懂
23:11:13 From mrj : goland有什么内置的lint工具吗
23:11:17 From mai yang : 连续按两次 shift
23:11:25 From mai yang : Double shift 不是double ctrl
23:12:09 From Lewis : run everywhere 就是double ctrl
23:12:11 From Eiger : cmder
23:13:11 From mrj : wondful
23:13:14 From mai yang : 哦
23:13:25 From Lewis : 好像是去年底更新出来的吧
23:17:09 From Dominic : wow
23:17:17 From mai yang : 这个厉害了~
23:17:22 From mrj : 牛逼
23:17:28 From yongping zhao : 666
23:17:51 From g : 666
23:18:42 From mrj : 还能直接下载
23:19:02 From lidedongsn : 人性化
23:19:06 From tianyi wang : 这个真不知道
23:19:33 From Lewis : download go sdk 我之前试了几次都加载不出来
23:19:51 From Lewis : 是国内网络环境都问题吗
23:19:52 From mrj : 可能是从golang.org download的
23:20:18 From tianyi wang : ha介绍代理了
23:21:50 From mrj : 私有仓库 这个问题之前还真遇到了
23:22:07 From mrj : goland 一直提示go list -m 找不到包
23:22:22 From Lewis : vgo设置里面的proxy历史记录可以删除吗?写错过一次,强迫症表示看着受不了
23:23:22 From tianyi wang : 对我也遇到过,go list -m一直到找不到goole.org的那几个包
23:24:55 From Kevin Bai : 这个实用
23:25:16 From Dominic : 类似 workspace 的概念
23:25:41 From bruce : 这个实用
23:25:51 From Lewis : 之前这样弄搞得改错项目文件过😂
23:27:13 From bruce : hehe
23:28:17 From mrj : 如果这个能图形化就好了
23:31:27 From mai yang : 运行出来的怎么结束掉呢?
23:31:41 From mai yang : double ctrl 之后的进程,怎么中断掉?
23:32:18 From mai yang : 必须点击终止键?不能快捷键 ctrl+c?
23:32:48 From mai yang : File watchers : golint goimports
23:33:02 From mai yang : gofmt
23:33:46 From Dominic : watcher 用多了 cpu 会爆吧😂
23:44:15 From tianyi wang : 有做rust的IDE的计划吗
观看视频
{{< youtube id="iCGsPN4qgdM" >}}
Night 51 2019 07 18 Sync Errgroup (`content/night/51-2019-07-18-sync-errgroup.md`)
desc: Go 夜读之 sync/errgroup 源码阅读 title: 第 51 期 Go 夜读之 sync/errgroup 源码阅读 date: 2019-07-18T21:10:00+08:00 author: mai
golang.org/x/sync/errgroup
errgroup 唯一的坑是for循环里千万别忘了 i, x := i, x,以前用 waitgroup 的时候都是 go func 手动给闭包传参解决这个问题的,errgroup 的.Go没法这么干,犯了好几次错才改过来"
观看视频
{{< youtube id="CQOZtzmgLvw" >}}
Night 52 2019 07 25 Httprouter Guide (`content/night/52-2019-07-25-httprouter-guide.md`)
desc: Go 夜读之 httprouter 简介 title: 第 52 期 Go 夜读之 httprouter 简介 date: 2019-07-25T21:10:00+08:00 author: 曹大
httprouter 简介
详细内容,可以查看 https://cch123.github.io/httprouter/
观看视频
{{< youtube id="BLYX6SKxFJA" >}}
Night 53 2019 08 01 Build In Delete From Map In Go (`content/night/53-2019-08-01-build-in-delete-from-map-in-go.md`)
desc: Go 夜读之 build in func delete from map title: 第 53 期 Go 夜读之 build in func delete from map date: 2019-08-01T21:10:00+08:00 author: mai
Go 夜读第 53 期 delete from map in go
突然有一个需求要删除 map 中的一些过滤数据。
由此查阅了一些资料,然后促成此次分享。
PPT:
build-in func delete from map in go.pptx
分享时间
2019-08-01 21:00:00
分享平台
zoom 在线直播 - https://zoom.us/j/6923842137
参考资料
- https://stackoverflow.com/questions/1736014/delete-mapkey-in-go
- https://blog.cyeam.com/json/2017/11/02/go-map-delete
- https://blog.golang.org/go-maps-in-action
- https://gobyexample.com/maps
- https://stackoverflow.com/questions/23229975/is-it-safe-to-remove-selected-keys-from-map-within-a-range-loop
- https://www.cnblogs.com/qcrao-2018/p/10903807.html
- https://appdividend.com/2019/05/12/golang-maps-tutorial-with-examples-maps-in-go-explained/
- https://www.jianshu.com/p/92e9efec8688
- https://www.reddit.com/r/golang/comments/5tfx7i/why_delete_doesnt_return_a_bool/
- https://www.liwenzhou.com/posts/Go/08_map/
- https://github.com/EDDYCJY/blog/tree/master/map
观看视频
{{< youtube id="sn810EcpOVs" >}}
Night 54 2019 08 14 Tidb Sql Tools (`content/night/54-2019-08-14-tidb-sql-tools.md`)
desc: Go 夜读之 TiDB SQL 兼容性测试工具简介 title: 第 54 期 Go 夜读之 TiDB SQL 兼容性测试工具简介 date: 2019-08-14T21:00:00+08:00 author: PingCAP
Go 夜读第 54 期 TiDB SQL 兼容性测试工具简介
本次分享包含两方面内容:
通过 MySQL yacc 文件生成 SQL Cases,并用于 TiDB 的兼容性测试的原理讲解。
TiDB Parser 兼容性社区活动介绍,手把手的演示如何参与本次社区活动。
(彩蛋:Parser Working Group 成立了,有兴趣的小伙伴可以看视频然后扫描加入。)
分享时间
2019-08-14 21:00:00
分享平台
zoom 在线直播 - https://zoom.us/j/6923842137
参考资料
观看视频
{{< youtube id="Hmu3F1Vafqc" >}}
Night 55 2019 08 15 Go Webassembly Guide (`content/night/55-2019-08-15-go-webassembly-guide.md`)
desc: Go 夜读之 Go&WebAssembly 简介 title: 第 55 期 Go&WebAssembly 简介 date: 2019-08-15T21:00:00+08:00 author: 柴大
Go 夜读第 55 期 Go&WebAssembly 简介
WebAssembly 简介
WebAssembly 是一种新兴的网页虚拟机标准,它的设计目标包括:高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。
根据 Ending 定律:⼀切可被编译为 WebAssembly 的,终将被编译为 WebAssembly。
本次分享 Go&WebAssembly 相关的用法。
分享时间
2019-08-15 21:00:00
分享平台
zoom 在线直播 - https://zoom.us/j/6923842137
更多讨论
FelixSeptem:补充一下 go 官方给的 wiki https://github.com/golang/go/wiki/WebAssembly 以及
WebAssembly 官网 https://webassembly.org/
个人比较倾向于对于 https://github.com/gopherjs/gopherjs 来比较理解,相对于 go->js (包括 react 等等) 的方案,WebAssembly 带来的异同是什么?
chai2010 :@FelixSeptem wasm 和 gopherjs 最大的差异:wasm 是官方支持,同时 wasm 是国际标准是其它语言认可的中间格式。
以前虽然很多工具输出 js,那是因为没有 wasm 可以选择。
现在有了 wasm,大家肯定只支持 wasm 而逐渐弱化 js 的支持。
毕竟 wasm 虚拟机实现比 v8 简单多了,性能又可以秒杀 js。
wasm 最大的潜力是在浏览器之外,甚至可以想象成一个轻量化的 Docker 环境。
我觉得这个才是 wasm 真正有意思的地方,wasm 对于 js 完全属于降维打击。
changkun:还没有在生产环境使用过 wasm。从给的马里奥的例子来看,go wasm 本质上是分发由 Go 编译好 .wasm,而 Go 端的本质就是提供了一些能够解释为 wasm 的 utils。不太清楚会不会在分享中提及这一本质。
长远来看,这个 .wasm 文件在特性支持的情况下最终会包含完整的 Go 运行时,
但 go wasm 并没有明确在 web 场景下为什么一定需要它,当然不可否认它的确为兼容并移植 Go 代码来发展 web 应用带来了便捷,但前提是我们必须有足够多基础设施是基于 Go 的,但游戏并没有,非常希望看到一些能够说服用 Go 写 wasm 而不是其他语言(C/C++ 有着丰富的图形资产,而 Go 在这方面的积累为 0,甚至连马里奥的例子都是依赖一个 cgo 对 c sdl2 renderer 的封装)编译 wasm 的论点。
参考资料
观看视频
{{< youtube id="O_FJgYKOBYQ" >}}
Night 56 2019 08 22 Channel Select In Go (`content/night/56-2019-08-22-channel-select-in-go.md`)
desc: Go 夜读之 channel & select 源码分析 title: 第 56 期 channel & select 源码分析 date: 2019-08-22T21:00:00+08:00 author: 欧长坤
Go 夜读第 56 期 channel & select 源码分析
内容简介
Go 语言除了提供传统的互斥量、同步组等同步原语之外,还受 CSP 理论的影响,提供了 channel 这一强有力的同步原语。本次分享将讨论 channel 及其相关的 select 语句的源码,并简要讨论 Go 语言同步原语中的性能差异与反思。
内容大纲
- 同步原语概述
- channel/select 回顾
- channel 的结构设计
- channel 的初始化行为
- channel 的发送与接收过程及其性能优化
- channel 的回收
- select 的本质及其相关编译器优化
分享地址
2019.08.22, 20:30 ~ 21:30, UTC+8
进一步阅读的材料
[Ou, 2019] channel & select 源码分析
分享内容的 PPT
[Ou, 2018] Go 源码研究
分享者写的一本 Go 源码分析
[Mullender and Cox, 2008] S. Mullender and R. Cox, Semaphores in Plan 9
影响 Go 语言信号量设计的一篇文章
[Drepper, 2003] U. Drepper, Futexes are Tricky
第一篇正确实现 Linux Futex 机制的文章
[Vyukov, 2014a] D. Vyukov, Go channels on steroids, January 2014
无锁式 channel 的设计提案
[Vyukov, 2014b] D. Vyukov, runtime: lock-free channels, October 2014
关于无锁式 channel 的讨论、早期实现与现状
[Hoare, 2015] C. A. R. Hoare, Communicating Sequential Processes. May 18, 2015
有关 CSP 理论的一切,与早期 1978 年的版本相比更为完善和严谨
[Creager, 2016] D. Creager, An oversimplified history of CSP, 2016
CSP 理论的极简史
更多见:https://github.com/talkgo/night/issues/450
本次分享的 Q&A 以及几个未在分享过程中进行回答的问题:
Q: buffer 队列的顺序是先进后出吗?
A: 不对,channel 中的 ring buffer 是一种先进先出 FIFO 的结构。
Q: channel 也是基于共享内存实现的吗?
A: 没错,从实现上来看,具体而言,channel 是基于对 buffer 这一共享内存的实体来实现的消息通信,每次对所共享内存区域的操作都需要使用互斥锁(个别 fast path 除外)。
Q: 创建 channel 所申请的内存,在其被 close 后何时才会释放内存?
A: 需要等待垃圾回收器的配合(GC)。举例来说,对于某个 channel 而言,所通信双方的 goroutine 均已进入 dead 状态,则垃圾回收器会将 channel 创建时申请的内存回收到待回收的内存池,在当下一次用户态代码申请内存时候,会按需对内存进行清理(内存分配器的工作原理);由此可见:如果我们能够确信某个 channel 不会使其通信的 goroutine 发生阻塞,则不必将其关闭,因为垃圾回收器会帮我们进行处理。
Q: 请问是否可以分享一下带中文注释的代码?
A: 带中文注释的代码可以在这个仓库的 gosrc 文件夹下看到。
Q: 能详细说明一下使用 channel 发送指针产生数据竞争的情况吗?
A: 这个其实很好理解,若指针作为 channel 发送对象的数据,指针本身会被 channel 拷贝,但指针指向的数据本身并没有被拷贝,这时若两个 goroutine 对该数据进行读写,仍然会发生数据竞争;请参考此例 (使用 -race 选项来检测竞争情况)。因此,除非在明确理解代码不会发生竞争的情况下,一般不推荐向 channel 发送指针数据。
Q: 分享的 PPT 的地址在哪儿?
A: 链接在这里,此 PPT 允许评论,若发现任何错误,非常感谢能够指出其错误,以免误导其他读者。
Q: 请问分享的视频链接是什么?
A: 有两个渠道,YouTube, bilibili,bilibili 中视频声画不同步,可使用 B 站的播放器进行调整,或推荐使用 YouTube 观看。
Q: Go 语言中所有类型都是不安全的吗?
A: 这个问题不太完整,提问者应该是想说 Go 中的所有类型都不是并发安全的。这个观点不对,sync.Map 就是一个并发安全的类型(当然如果你不考虑标准库的话,那么内建类型中,channel 这个类型也是并发安全的类型)。
Q: 如果 channel 发送的结构体太大,会不会有内存消耗过大的问题?
A: 取决于你结构体本身的大小以及你所申请的 buffer 的大小。通常在创建一个 buffered channel 时,该 channel 消耗的内存就已经确定了,如果内存消耗太大,则会触发运行时错误。我们更应该关注的其实是使用 channel 发送大小较大的结构体产生的性能问题,因为消息发送过程中产生的内存拷贝其实是一件非常耗性能的操作。
Q: select{} 的某个 case 发生阻塞则其他 case 也不会得到执行吗?
A: 对的。包含多个 case 的 select 是随机触发的,且一次只有一个 case 得到执行。极端情况下,如果其中一个 case 发生永久阻塞,则另一个 case 永远不会得到执行。
Q: select 中使用的 heap sort 如何保证每个 case 得到均等的执行概率呢?是否可能会存在一个 case 永远不会被执行到?
A: 理论上确实是这样。但是代码里生成随机数的方法保证了是均匀分布,也就是说一个区间内的随机数,某个数一直不出现的概率是零,而且还可以考虑伪随机数的周期性,所以所有的 case 一定会被选择到,关于随机数生成的具体方法,参见 runtime.fastrand 函数。
Q: lockorder 的作用是什么?具体锁是指锁什么?
A: lockorder 是根据 pollorder 和 channel 内存地址的顺序进行堆排序得到的。 pollorder 是根据 random shuffle 算法得到的,而 channel 的内存地址其实是内存分配器决定的,考虑到用户态代码的随机性,因此堆排序得到的 lockorder 的结果也可以认为是随机的。lockorder 会按照其排序得到的锁的顺序,依次对不同的 channel 上锁,保护其 channel 不被操作。
Q: buffer 较大的情况下为什么没有使用链表结构?
A: 这个应该是考虑了缓存的局部性原理,数组具有天然的连续内存,如果 channel 在频繁的进行通信,使用数组自然能使用 CPU 缓存局部性的优势提高性能。
Q: chansend 中的 fast path 是直接访问 qcount 的,为什么 chanrecv 中却使用了 atomic load 来读取 qcount 和 closed 字段呢?
A: 这个这两个 fast path 其实有炫技的成分太高了,我们需要先理解这两个 fast path 才能理解为什么这里一个需要 atomic 操作而另一个不需要。
首先,他们是针对 select 语句中非阻塞 channel 操作的的一种优化,也就是说要求不在 channel 上发生阻塞(能失败则立刻失败)。这时候我们要考虑关于 channel 的这样两个事实,如果 channel 没有被 close:
- 那么不能进行发送的条件只可能是: unbuffered channel 没有接收方( dataqsiz 为空且接受队列为空时),要么 buffered channel 缓存已满(dataqsiz != 0 && qcount == dataqsize)
- 那么不能进行接受的条件只可能是:unbuffered channel 没有发送方( dataqsiz 为空且发送队列为空),要么 buffered channel 缓存为空(dataqsiz != 0 && qcount == 0)
理解是否需要 atomic 操作的关键在于:atomic 操作保证了代码的内存顺序,是否发生指令重排。
由于 channel 只能由未关闭状态转换为关闭状态,因此在 !block 的异步操作中,
第一种情况下,channel 未关闭和 channel 不能进行发送之间的指令重排是能够保证代码的正确性的,因为:在不发生重排时,「不能进行发送」同样适用于 channel 已经 close。如果 closed 的操作被重排到不能进行发送之后,依然隐含着在判断「不能进行发送」这个条件时候 channel 仍然是未 closed 的。
但第二种情况中,如果「不能进行接收」和 channel 未关闭发生重排,我们无法保证在观察 channel 未关闭之后,得到的 「不能进行接收」是 channel 尚未关闭得到的结果,这时原本应该得到「已关闭且 buf 空」的结论(chanrecv 应该返回 true, false),却得到了「未关闭且 buf 空」(返回值 false, false),从而报告错误的状态。因此必须使此处的 qcount 和 closed 的读取操作的顺序通过原子操作得到顺序保障。
Q: 听说 cgo 性能不太好,是真的吗?
A: 是的,至少我的经验的结论是 cgo 性能非常差。因为每次进入一个 cgo 调用相当于进入 system call,这时 goroutine 会被抢占,从而导致的结果就是可能会很久之后才被重新调度,如果此时我们需要一个密集的 cgo 调用循环,则性能会非常差。
Q: 看到你即写 C++ 也研究 Go 源码,还做深度学习,能不能分享以下学习的经验?
A: 老实说我已经很久没(正儿八经)写 C++ (的项目)了,写 C++ 那还是我本科时候的事情,那个时候对 C++ 的理解还是很流畅的,但现在已经感觉 C++ 对于我编程的心智负担太高了,在编写逻辑之外还需要考虑很多与之不相关的语言逻辑,大部分时间其实浪费在这上面了,时间稍长就容易忘记一些特性。加上我后来学了 Go ,就更不想用 C++ 了。另外,我读硕士的时候主要在研究机器学习,主要就是在写 python 脚本。所以我暂时也没什么比较系统的经验,如果非要回答的话,我的一个经验就是当(读源码)遇到问题之后硬着头皮走下去,当积累到一定程度之后在回过头去审视这些问题,就会发现一切都理所当然。
Q: 你是怎么读 Go 源码的?
A: 最开始的时,我选择了一个特定的版本,将想看的源码做了一个拷贝(主要是运行时的代码,刨去了 test、cgo、架构特定等代码),而后每当 Go 更新一个版本时,都用 GitHub Pull request 的 diff 功能,去看那些我关心的代码都发生了哪些改变。当需要我自身拷贝的代码时,其实会发现工作量并不是很大。刚接触 Go 源码的时候其实也是一脸懵,当时也并没有太多 Go 的编码经验,甚至连官方注释都看不太明白,后来硬着头皮看了一段时间,就慢慢的适应了。
Q: 有没有什么比较好的英文的(Go 相关的)资料推荐?
A: 其实我订阅的 Go 的信息并不多,主要原因还是信息量太多,平时太忙应付不过来,所以只订阅了几个比较知名的博客,比如 www.ardanlabs.com/blog, dave.cheney.net 和一些 medium 上比较大众的跟 Go 有关的 channel;我倒是经常在地铁或睡觉前听一个叫做 Go Time 的 Podcast,这个 Podcast 是有 Go 团队的成员参与的,很值得一听。另外再推荐一些与 Go 不是强相关的技术类书籍,参见 书籍推荐。
观看视频
{{< youtube id="d7fFCGGn0Wc" >}}
Night 57 2019 08 29 Sync Semaphore (`content/night/57-2019-08-29-sync-semaphore.md`)
desc: Go 夜读之 sync/semaphore 源码浅析 title: 第 57 期 sync/semaphore 源码浅析 date: 2019-08-29T21:00:00+08:00 author: Felix
Go 夜读第 57 期 sync/semaphore 源码浅析
内容简介
主要分析 golang.org/x/sync/semaphore 相关代码和 semaphore 部分使用场景。
内容大纲
- semaphore 定义
- 源码分析
- Q&A
分享地址
2019.08.29, 21:00 ~ 21:40, UTC+8
进一步阅读的材料
补充资料
更多见:https://github.com/talkgo/night/issues/456
观看视频
{{< youtube id="VEtdLBFY_y4" >}}
Night 58 2019 09 05 Whats New In Go1.13 (`content/night/58-2019-09-05-whats-new-in-go1.13.md`)
desc: Go 夜读之 What's new in Go 1.13? title: 第 58 期 What's new in Go 1.13? date: 2019-09-05T21:05:00+08:00 author: Go 夜读 SIG 小组
Go 夜读第 58 期 What's new in Go 1.13?
内容简介
主要介绍了刚刚发布的 Go 1.13 Release 的内容。
内容大纲
- Go modules
- Toolchain
- Runtime
- CoreLibrary Improve Performance
- Q&A
分享地址
2019-09-05, 21:00 ~ 22:10, UTC+8
进一步阅读的材料
更多见:https://github.com/talkgo/night/issues/465
观看视频
{{< youtube id="jVUy47OrpRk" >}}
Night 59 2019 09 12 Real World Go Concurrency Bugs In Paper Reading (`content/night/59-2019-09-12-real-world-go-concurrency-bugs-in-paper-reading.md`)
desc: Go 夜读之 Real-world Go Concurrency Bugs title: 第 59 期 Real-world Go Concurrency Bugs date: 2019-09-12T21:00:00+08:00 author: Go 夜读 SIG 小组
Go 夜读第 59 期 Real-world Go Concurrency Bugs
内容简介
Go 语言鼓励其用户多使用基于消息传递的同步原语 channel,但也不排斥其用户使用基于内存共享的同步原语,提供了诸如 sync.Mutex 等互斥原语。在过去十年的时间里,Go 的实践者不断思考着这些问题:哪种同步原语更加优秀?究竟什么场景下应该使用何种同步原语?哪类同步原语能够更好的保证数据操纵的正确性?哪类同步原语对程序员的心智负担较大?何种同步原语更容易产生程序 Bug?channel 是一种反模式吗?什么类型的 Bug 能够更好的被 Bug 检测器发现?……
[Tu et al., 2019] 调研了包括 Docker, Kubernetes, gRPC 等六款主流大型 Go 程序在演进过程中出现的 171 个与同步原语相关的 Bug,并给出了一些有趣的见解。本次分享将讨论 [Tu et al. 2019] 的研究论文。
内容大纲
- Go 常见的并发模式与论文的研究背景
- 论文的研究方法
- Go 并发 Bug 的分类及部分主要结论
- 阻塞式 Bug
- 非阻塞式 Bug
- Go 运行时死锁、数据竞争检测器对 Bug 的检测能力与算法原理(如果时间允许)
- 论文的结论、争议与我们的反思
分享地址
2019-09-12, 21:00 ~ 22:10, UTC+8
进一步阅读的材料
- [Ou, 2019a] Real-world Go Concurrency Bugs PPT
- 本次分享的 PPT: 这里
- [Pike, 2012] Go Concurrency Patterns
- Rob Pike 关于 「Go 并发模式」的 PPT
- [Gerrand, 2013] Advanced Go Concurrency Patterns
- Sameer Ajmani 关于 「Go 并发模式进阶」的 PPT
- [Tu et al., 2019] Understanding Real-World Concurrency Bugs in Go
- 本次分享讨论的论文
- 论文作者的 PPT
- Bug Table
- 论文中对应的 GitHub 仓库:https://github.com/system-pclub/go-concurrency-bugs
- 与论文作者的一次面谈记录:https://www.jexia.com/en/blog/golang-error-proneness-message-passing/
- Hacker News 对本文的讨论 https://news.ycombinator.com/item?id=19280927
- [Utahn, 2019] Go channels are bad and you should feel bad
- [Ou, 2019b] Go 夜读 第 56 期:channel & select 源码分析
请点击:https://github.com/talkgo/night/issues/464
QA
Q: 可以再详细说一下 lift 指标吗?
A: 可以从两种不同的角度来思考这个指标。
- 借鉴 person 相关性系数(余弦相似性) |X·Y|/(|X|*|Y|)
- 借鉴 Bayes 公式 P(B|A) = P(AB)/P(A)
- Lift(cause, fix) = 导致阻塞的 cause 且使用了 fix 进行修复的概率 除以 cause 的概率乘以 fix 的概率 = P(cause, fix) / (P(cause)P(fix)) = P(cause|fix)/P(cause)
- 接近 1 时,说明 fix 导致 cause 的概率接近 cause 自己的概率,即 P(cause|fix) 约等于 P(cause) 于是 fix 和 cause 独立
- 大于 1 时,说明 fix 导致 cause 的概率比 cause 自己的概率大,即 P(cause|fix) > P(cause) => P(fix | cause) > P(fix),即 cause 下 fix 的概率比 fix 本身的概率大,正相关
- 小于 1 时,同理,负相关
Q: 可以贴一下提到的两篇相关文献吗?
A: 论文引用了两篇很硬核的形式化验证的论文:
Julien Lange, Nicholas Ng, Bernardo Toninho, and Nobuko Yoshida. Fencing off go: Liveness and safety for channel-based programming. In Proceedings ofthe 44th ACMSIGPLANSymposium on Principles of Programming Languages (POPL ’17), Paris, France, January 2017.
Julien Lange, Nicholas Ng, Bernardo Toninho, and Nobuko Yoshida. A static verification framework for message passing in go using be- havioural types. In IEEE/ACM40th International Conference on Software Engineering (ICSE ’18), Gothenburg, Sweden, June 2018.
Q: 作者还分享了其他语言的关于并发 Bug 的论文,比如 Rust。
A: 地址在这里,但是思路完全一致,可以直接扫一眼结论。
Q: 能否将 CSP 和 Actor 模型进行一下简单比较?
A: CSP 和 Actor 的本质区别在于如何对消息传递进行建模。Actor 模型强调每个通信双方都有一个“信箱”,传递消息的发送方对将这个消息发给谁是很明确的,这种情况下 Actor 的信箱必须能够容纳不同类型的消息,而且理论上这个信箱的大小必须无穷大,很像你想要送一件礼物给别人,你会直接把礼物递给这个人,如果这个人不在,你就扔到他家的信箱里;CSP 需要有一个信道,因此对发送方而言,其实它并不知道这个消息的接收方是谁,更像是你朝大海扔了一个漂流瓶,大海这个信道根据洋流将这个漂流瓶传递给了其他正在观察监听大海的人。
Q: 读论文的目标是什么?
A: 我读论文主要有两个目标:1. 了解论文的研究方法,因为研究方法可能可以用在我未来的研究中;2. 了解论文的整体思路,因为论文很多,思路远比它们的结果对我未来自己的研究更重要。
Q: 去哪儿找这类论文?
A: 我们这次讨论的论文是我偶然在 Go 语言 GitHub 仓库的 Wiki 上看到的;一般情况下我会订阅 ArXiv,然后定期浏览新发出来的文章。
观看视频
{{< youtube id="WZUii-Czaps" >}}
Night 6 2018 05 17 Strings Part4 (`content/night/6-2018-05-17-strings-part4.md`)
title: 第 6 期 2018-05-17 线下活动 - Go 标准包阅读 date: 2018-05-17T11:49:10+08:00
参与人数: 12 人
Go 标准包阅读
Go 版本:go 1.10.1
strings
- strings.go
问题清单
以下是我们在阅读过程中的一些问题,希望可以引起大家的关注,也欢迎大家提出自己的理解,最好可以给以文章总结。
- // Remove if golang.org/issue/6714 is fixed
- bp := copy(b, a[0])
- return len(s) >= len(prefix) && s[0:len(prefix)] == prefix 各种开发语言都有的短路机制;字符串底层也是可以用作切片的;
- 为什么要判断这个错误: if c == utf8.RuneError
- c -= 'a' - 'A' (小写转大写的算法)
- // Since we cannot return an error on overflow,
// we should panic if the repeat will generate
// an overflow.
// See Issue golang.org/issue/16237 - truth
- asciiSet (bitset 标记位,存在标记为1)
传一个字符串,把字符串包含的ascii,对应的256位,进行映射。
Unicode 包很多都看不懂。
func isSeparator(r rune) bool
延伸阅读
Night 60 2019 09 19 Ipfs Guide (`content/night/60-2019-09-19-ipfs-guide.md`)
desc: Go 夜读之 IPFS 星际文件系统 title: 第 60 期 IPFS 星际文件系统 date: 2019-09-19T21:05:00+08:00 author: xcshuan
Go 夜读第 60 期 IPFS 星际文件系统
背景介绍
传统的 HTTP 都是通过资源定位符来定位,在服务器关闭后,有些数据可能会永远丢失,而且如果某客户离服务器比较远,则可能延时较高。IPFS 提出使用基于内容寻址,只要拥有 hash 且网络上有人存储此数据,即可获得数据,同时自带 CDN 效果(热数据会自动分散)。
内容简介
主要介绍一下 ipfs 的基本思想与使用,并分析源码结构以及粗略介绍相关兄弟项目(如 multiformats,filecoin 等)
内容大纲
- IPFS 的底层技术原理。
- IPFS 源码概述。
- 超越 IPFS - 区块链存储简述。
- Q&A。
分享地址
2019-09-19, 21:00 ~ 22:10, UTC+8
进一步阅读的材料
请点击:https://github.com/talkgo/night/issues/460
Q&A 总结
IPFS 是如何组网的?
答:IPFS 的底层网络库是 Libp2p,Libp2p 的路由算法是 S-KadDHT(分布式哈希表),只要能连接到网络中的几个节点,通过节点发现与交换,很容易就能进入到网络里,所以需要设置 bootstrap 节点作为连接种子。对于个人或公司想用 IPFS 组网,可以用swarm.key组建一个私网(需要指定一bootstrap),即可实现内部的 IPFS 网络。IPFS 如何实现模糊搜索?
答:首先 IPFS 的 DAG 节点里面都是有一个 name 项,此外还有一些其他的信息可以解析,这样的话可以爬取这些元数据信息,用一些搜索引擎工具即可模糊搜索,开源实现:https://github.com/ipfs-search/ipfs-search,可以用来当做参考。IPFS 其他的相关资料。
中文资料,有一本《IPFS 原理与实践》,其次有一个 github 仓库:https://github.com/xipfs/IPFS-Internals,还有 IPFS 的各种命令解释:http://cw.hubwiz.com/card/c/ipfs/1/1/1/
英文首先有官方文档:https://github.com/ipfs/specs,https://github.com/filecoin-project/specs,一个教程:https://flyingzumwalt.gitbooks.io/decentralized-web-primer 。
观看视频
{{< youtube id="7xEWKaTE2TI" >}}
Night 61 2019 09 26 Go Module Goproxy Cn (`content/night/61-2019-09-26-go-module-goproxy-cn.md`)
desc: Go 夜读 之 Go Modules、Go Module Proxy 和 goproxy.cn #468 title: 第 61 期 Go Modules、Go Module Proxy 和 goproxy.cn #468 date: 2019-09-26T21:00:00+08:00 author: 盛傲飞
Go 夜读第 61 期 Go Modules、Go Module Proxy 和 goproxy.cn #468
内容简介
Go 1.11 推出的模块(Modules)为 Go 语言开发者打开了一扇新的大门,理想化的依赖管理解决方案使得 Go 语言朝着计算机编程史上的第一个依赖乌托邦(Deptopia)迈进。随着模块一起推出的还有模块代理协议(Module proxy protocol),通过这个协议我们可以实现 Go 模块代理(Go module proxy),aka 依赖镜像。Go 1.13 的发布为模块带来了大量的改进,所以模块的扶正就是这次 Go 1.13 发布中开发者能直接感觉到的最大变化。Go 1.13 中的 GOPROXY 环境变量拥有了一个在中国大陆无法访问到的默认值 proxy.golang.org,经过大家在 https://github.com/golang/go/issues/31755 中激烈的讨论(有些人甚至将话提上升到了“自由世界”的层次),最终 Go 核心团队仍然无法为中国开发者提供一个可在中国大陆访问的官方模块代理。为了今后中国的 Go 语言开发者能更好地进行开发,七牛云推出了非营利性项目 goproxy.cn,其目标是为中国和世界上其他地方的 Gopher 们提供一个免费的、可靠的、持续在线的且经过 CDN 加速的模块代理。可以预见未来是属于模块化的,所以 Go 语言开发者能越早切入模块就能越早进入未来。如果说 Go 1.11 和 Go 1.12 时由于模块的不完善你不愿意切入,那么 Go 1.13 你则可以大胆地开始放心使用。本次分享将讨论如何使用模块和模块代理,以及在它们的使用中会常遇见的坑,还会讲解如何快速搭建自己的私有模块代理,并简单地介绍一下七牛云推出的 goproxy.cn 以及它的出现对于中国 Go 语言开发者来说重要在何处。
内容大纲
- Go Modules 简介
- 快速迁移项目至 Go Modules
- 使用 Go Modules 时常遇见的坑
- 坑 1:判断项目是否启用了 Go Modules
- 坑 2:管理 Go 的环境变量
- 坑 3:从 dep、glide 等迁移至 Go Modules
- 坑 4:拉取私有模块
- 坑 5:更新现有的模块
- 坑 6:主版本号
- Go Module Proxy 简介
- 快速搭建私有 Go Module Proxy
- Goproxy 中国(goproxy.cn)
分享者自我介绍
盛傲飞,两个多月前刚本科毕业,目前刚毕业旅行归来还未找工作,从事 Go 语言开发 4 年左右,开源爱好者,goproxy.cn 的作者。
分享时间
2019-09-26 21:00 UTC+8
分享地址
录播地址
Bilibili(音视频不同步,正在修复……):https://www.bilibili.com/video/av69111199
YouTube:https://youtu.be/H3LVVwZ9zNY
Slides
https://docs.google.com/presentation/d/1LRs_D-IlrSU-ZrJN7KiBhNmCY5tWXH1b2CyDF7jvClY/edit?usp=sharing
参考资料
- https://github.com/golang/go/wiki/Modules
- https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more
- https://blog.golang.org/versioning-proposal
- https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md
- https://youtu.be/F8nrpe0XWRg
- https://golang.org/doc/go1.11#modules
- https://blog.golang.org/modules2019
- https://golang.org/doc/go1.12#modules
- https://blog.golang.org/using-go-modules
- https://blog.golang.org/migrating-to-go-modules
- https://blog.golang.org/module-mirror-launch
- https://golang.org/doc/go1.13#modules
- https://studygolang.com/topics/9994
- https://studygolang.com/topics/10014
直播中的 Q&A 环节内容
问:如何解决 Go 1.13 在从 GitLab 拉取模块版本时遇到的,Go 错误地按照非期望值的路径寻找目标模块版本结果致使最终目标模块拉取失败的问题?
答:GitLab 中配合 go get 而设置的 <meta> 存在些许问题,导致 Go 1.13 错误地识别了模块的具体路径,这是个 Bug,据说在 GitLab 的新版本中已经被修复了,详细内容可以看 https://github.com/golang/go/issues/34094 这个 Issue。然后目前的解决办法的话除了升级 GitLab 的版本外,还可以参考 https://github.com/talkgo/night/issues/468#issuecomment-535850154 这条回复。
问:使用 Go modules 时可以同时依赖同一个模块的不同的两个或者多个小版本(修订版本号不同)吗?
答:不可以的,Go modules 只可以同时依赖一个模块的不同的两个或者多个大版本(主版本号不同)。比如可以同时依赖 example.com/[email protected] 和 example.com/foobar/[email protected],因为他们的模块路径(module path)不同,Go modules 规定主版本号不是 v0 或者 v1 时,那么主版本号必须显式地出现在模块路径的尾部。但是,同时依赖两个或者多个小版本是不支持的。比如如果模块 A 同时直接依赖了模块 B 和模块 C,且模块 A 直接依赖的是模块 C 的 v1.0.0 版本,然后模块 B 直接依赖的是模块 C 的 v1.0.1 版本,那么最终 Go modules 会为模块 A 选用模块 C 的 v1.0.1 版本而不是模块 A 的 go.mod 文件中指明的 v1.0.0 版本。这是因为,Go modules 认为只要主版本号不变,那么剩下的都可以直接升级采用最新的。但是如果采用了最新的结果导致项目 Break 掉了,那么 Go modules 就会 Fallback 到上一个老的版本,比如在前面的例子中就会 Fallback 到 v1.0.0 版本。
问:在 go.sum 文件中的一个模块版本的 Hash 校验数据什么情况下会成对出现,什么情况下只会存在一行?
答:通常情况下,在 go.sum 文件中的一个模块版本的 Hash 校验数据会有两行,前一行是该模块的 ZIP 文件的 Hash 校验数据,后一行是该模块的 go.mod 文件的 Hash 校验数据。但是也有些情况下只会出现一行该模块的 go.mod 文件的 Hash 校验数据,而不包含该模块的 ZIP 文件本身的 Hash 校验数据,这个情况发生在 Go modules 判定为你当前这个项目完全用不到该模块,根本也不会下载该模块的 ZIP 文件,所以就没必要对其作出 Hash 校验保证,只需要对该模块的 go.mod 文件作出 Hash 校验保证即可,因为 go.mod 文件是用得着的,在深入挖取项目依赖的时候要用。
问:能不能更详细地讲解一下 go.mod 文件中的 replace 动词的行为以及用法?
答:这个 replace 动词的作用是把一个“模块版本”替换为另外一个“模块版本”,这是“模块版本”和“模块版本(module path)”之间的替换,“=>”标识符前面的内容是待替换的“模块版本”的“模块路径”,后面的内容是要替换的目标“模块版本”的所在地,即路径,这个路径可以是一个本地磁盘的相对路径,也可以是一个本地磁盘的绝对路径,还可以是一个网络路径,但是这个目标路径并不会在今后你的项目代码中作为你“导入路径(import path)”出现,代码里的“导入路径”还是得以你替换成的这个目标“模块版本”的“模块路径”作为前缀。注意,Go modules 是不支持在“导入路径”里写相对路径的。举个例子,如果项目 A 依赖了模块 B,比如模块 B 的“模块路径”是 example.com/b,然后它在的磁盘路径是 ~/b,在项目 A 里的 go.mod 文件中你有一行 replace example.com/b => ~/b,然后在项目 A 里的代码中的“导入路径”就是 import "example.com/b",而不是 import "~/b",剩下的工作是 Go modules 帮你自动完成了的。然后就是我在分享中也提到了,exclude 和 replace 这两个动词只作用于当前主模块,也就是当前项目,它所依赖的那些其他模块版本中如果出现了你待替换的那个模块版本的话,Go modules 还是会为你依赖的那个模块版本去拉取你的这个待替换的模块版本。比如项目 A 直接依赖了模块 B 和模块 C,然后模块 B 也直接依赖了模块 C,那么你在项目 A 中的 go.mod 文件里的 replace c => ~/some/path/c 是只会影响项目 A 里写的代码中,而模块 B 所用到的还是你 replace 之前的那个 c,并不是你替换成的 ~/some/path/c 这个。
观看视频
{{< youtube id="H3LVVwZ9zNY" >}}
Night 62 2019 10 10 Go Micro Part1 (`content/night/62-2019-10-10-go-micro-part1.md`)
desc: Go 夜读 之 Go-Micro 微服务框架 Part 1 title: 第 62 期 Go-Micro 微服务框架 Part 1 date: 2019-10-10T21:00:00+08:00 author: printfcoder
Go 夜读第 62 期 Go-Micro 微服务框架 Part 1
内容简介
介绍Go-Micro的设计及其重要组件
内容大纲
- 什么是 Micro
- Micro 风格服务架构
- Go-Micro 框架的设计
- Go-Micro 主要的组件
- Go-Micro 的插件化
分享地址
2019-10-10 21:00 ~ 22:00, UTC+8
进一步阅读的材料
请点击:https://github.com/talkgo/night/issues/457
Q&A 总结
- micro 是Restful吗?
答:go-micro并不是一个web框架,不过go-micro中有web模块可以提供restful风格服务。
- srv里面是包含client和server吗?
答:每一个服务都会有client和server,服务要能调用其它服务就需要一个client,能接收请求就需要server。
- 为什么异构service互相调用一定要经过proxy,rpc不应该是编码和transport约定好本身就支持异构调用吗?
答:micro proxy并不支持互调,它提供一个go-micro特性的代理,其它非go-micro风格的服务通过这个代理加入go-micro体系,便可以通过proxy被其它服务调用,可以是http、grpc等。
我会在未来几天增加一篇专门介绍micro的文档在这里:micro proxy
- micro与k8s
答:这是一个常见的问题,micro会常与k8s、istio比较或联系,这是不公平的也是不合理的。主要在这么几个方面:
a) micro与k8s同时起步,或者说micro更早些,k8s的产生是基于容器技术的兴起,而容器需要管理与编排。K8s确实给大型服务集群提供了极好的运维平台,但是它在一定适度上并不是面向开发人员的工具,更多是面向运维人员的。
b) 基于a,Micro是面向开发人员的微服务框架,如果人们用了K8s,那应该就不要用micro。
c) 绝大部分开发者可以试问内心的需求:自己当下的服务运行需求,真的需要K8s吗?说句傲娇的话,如果不能看到micro的价值,那请不要使用micro,在K8s中使用micro,就像皇宫的大内总管,少了件最重要的东西。
- 为啥要 consul 换成 etcd
答:从4年的结合Consul经验来说,它工作得比较正常但并不尽如人意,它有太多功能我们用不到。consul更多是面向Hashicorp体系的服务,而Etcd则更纯粹是服务注册组件,是的,我们需要更纯粹的中间件。更多可以参考一篇不太细致的博文:deprecating-consul
- client 和server可以在一个go文件中吗?
答:可以,但是为什么要这么做哩?
- go-micro中如何使用链路追踪?
答:在go-micro中提供了装饰器Wrapper可以集成任何支持go语言的链路追踪插件。见:Tracing
- Go-Micro 设计初衷、目标以及未来发展方向?
答:Go-Micro 设计初衷是做一套面向开发人员的微服务框架,她要使用简单、扩展简单、管理简单。但是仍然有很长的路要走,从技术上讲,目标是要发展成Java界的Spring Cloud。
关于未来的发展,一个技术终究是要赚钱的,Micro公司的开发与发展目标,详见与Network
观看视频
{{< youtube id="ucTwnDB1m2U" >}}
Night 63 2019 10 17 Go Style And Go Advices (`content/night/63-2019-10-17-go-style-and-go-advices.md`)
desc: Go 夜读 之 Go 编码风格阅读与讨论 title: 第 63 期 Go 编码风格阅读与讨论 date: 2019-10-17T21:00:00+08:00 author: mai
Go 夜读第 63 期 Go 编码风格阅读与讨论
内容简介
本期主要是针对近期 uber-go/guide style 和 go-advices 的解读以及开发者讨论。
内容大纲
- Go CodeReview Comments
- Uber-go/style
- Go-advices
分享地址
2019-10-17 21:00:00 ~ 22:10:00, UTC+8
分享 Slides
https://docs.google.com/presentation/d/1MlzZJBK0Zq0VzJVC_AqSWmmlS4Of-8xY6NGZmfhKQXI/edit?usp=sharing
进一步阅读的材料
Go CodeReviewComments 翻译
Go 官方的建议已经涉及到非常方面:
- Gofmt
- Comment Sentences
- Contexts
- Copying
- Crypto Rand
- Declaring Empty Slices
- Doc Comments
- Don't Panic
- Error Strings
- Examples
- Goroutine Lifetimes
- Handle Errors
- Imports
- Import Dot
- In-Band Errors
- Indent Error Flow
- Initialisms
- Interfaces
- Line Length
- Mixed Caps
- Named Result Parameters
- Naked Returns
- Package Comments
- Package Names
- Pass Values
- Receiver Names
- Receiver Type
- Synchronous Functions
- Useful Test Failures
- Variable Names
gofmt
不管你是用什么开发工具,都推荐一定要配置 goimports。
Context
- Context 应该在函数的第一个参数;
- 不要将 Context 加到结构体中,而应该加一个 ctx 参数;
- 不要创建自定义的 Context 类型;
- Context 是不可变的,所以可以将相同的 ctx 传递给调用共享相同截止日期,取消信号,凭证,父跟踪等;
虽然官方已经说明了,但是也还是有不少公司或者开源项目有自己的设计和实现。
Declaring Empty Slices
var t[]string 比 t:= []string{} 更好
Imports
应该按系统库、内部库、第三方库分层分隔。
Indent Error Flow
if err != nil {
// error handling...
return // or continue, etc.
}
// other codex, err := f()
if err != nil {
// error handling...
return
}
// use x...Variable Names
- 局部变量应该越精简越好;
- 不通用的或者全局变量,应该描述更清楚的命名;
Uber Go 风格指南翻译
- Uber Go 风格指南 (译) by 徐旭
- Uber Go 语言编程规范 by legendtkl
- 【重磅】Uber Go 语言代码风格指南 by Go 中国
- Uber Go 语言编码规范 by TonyBai
上周刚出来,过了2天,就出现大量的翻译文章,也能够看出来 Go 语言虽然官方有 gofmt,以及 go vet 静态代码检测工具,但是也抵挡不住大家对于代码风格的热衷。
也说明大家还希望将代码风格更统一,追求更好的代码。
Go-advices
- Code
- Concurrency
- Performance
- Modules
- Build
- Testing
- Tools
- Misc
代码方面
var foo time.Duration比var fooMillis int64更好- 检查
defer中的 error - 用
%+v打印足够详细信息 - 小心 range
- map 中读取一个不存在的 key 不会 panic,建议:
value, ok := map[“no_key”] - 将 defer 移到顶部
Dave Cheney
- Practical Go: Real world advice for writing maintainable Go programs
- 中文翻译版(2018 年):https://www.flysnow.org/2018/12/04/golang-the-go-best-presentations.html
Q&A 总结
- 下划线开头 来声明 全局变量
很少见,也不太适用。
- 为什么建议channel尽量不加buffer?
按需分配。
- go.uber.org/atomic
库非常好,原子操作很方便。新增多种数据类型。
- package_test 比 package 好?
比较清晰,但是也有局限性,测试不了内部逻辑,类似于外部包调用。
- go test 指定 -count 可以消除偶然因素导致的不稳定结果。
-count=1 也可以消除 cache。
- 有没有认证或授权库的推荐?
github 上搜索即可,一般可以根据 star 数量和活跃情况来评判。
也可以去 godoc.org 搜索,查看 imports 引入数据来评判。
https://github.com/dgrijalva/jwt-go
观看视频
{{< youtube id="91YbbwlKZ2k" >}}
Night 64 2019 10 24 Go Runtime (`content/night/64-2019-10-24-go-runtime.md`)
desc: Go 夜读之深入浅出 Golang Runtime title: 第 64 期深入浅出 Golang Runtime date: 2019-10-24T21:00:00+08:00 author: 郝以奋@腾讯NOW直播
Go 夜读第 64 期深入浅出 Golang Runtime
内容简介
本次分享将会对 go runtime 的调度,内存分配,gc 做一些细节上的讲解,同时也需要参与者对 runtime 有一些初步了解。
内容大纲
- Golang Runtime 是什么,其发展历程;
- 调度的实质和关键数据结构,函数;
- 内存分配中 mspan, mheap, mcentral, mcache 等数据结构
- Golang GC 发展,Golang 三色标记实现的一些细节,元信息,写屏障,1.5 与 1.12 GC 的区别;
- 一点优化思路与问题排查思路;
- 总结及 question;
- 平时我看 runtime 代码的一些方式;
分享嘉宾
郝以奋,yifhao, 腾讯 NOW 直播后台开发,负责 NOW 直播 CPP+JAVA 双栈 -> Golang 转型:框架协同建设,业务功能定制,Go Mod 引入,服务模板,RPC 协议 Go Mod 化,服务模板,Golang 培训,文档等。
目前 NOW 直播后台有 300 多个 Go 服务。
分享信息
时间:2019-10-17 21:00:00 ~ 23:10:00, UTC+8
分享 Slides:https://github.com/Frank-Hust/share
回看视频
Q&A 总结
Q: 腾讯现在用go的多吗?
多, 至少 2000 人的级别了,对go的接受度挺高的,使用人数在迅速增加,当然大部分团队还是 cpp。
Q: 腾讯 NOW 直播 go 开发占比多少?
我们都是从其他语言转的,cpp,java->golang,一开始就写 go 的比较少。基本上学习一下,一个星期就可以开始写线上 go 服务了。目前新服务都是 go。
Q: 线程切换的开销
线程切换大概在几微秒级别,协程切换大概在百 ns 级别。
线程切换过程:
- 进入系统调用
- 调度器本身代码执行
- 线程上下文切换: PC, SP 等寄存器,栈,线程相关的一些局部变量,还涉及一些 cache miss 的情况;
- 退出系统调用
协程切换不需要进入和退出系统调用, 在进行上下文切换时也更轻量, 只需要切换几个寄存器, 协程 runtime.g 结构只有 40 多个字段, 而线程的 task struct 有大概 300 个字段.
可参考进程/线程上下文切换会用掉你多少CPU?
https://zhuanlan.zhihu.com/p/79772089
协程究竟比线程能省多少开销?
https://zhuanlan.zhihu.com/p/80037638
Q: 为啥是边缘触发, 而不是水平触发的方式?
因为网络操作 ready 和未 ready 对于协程来说就是状态的切换。
socket fd ready 了, 阻塞之上的协程就从 waiting 变成 runnable。
操作时 socket fd 未 ready,那协程就从 running 变成 waiting。
假如采取水平触发,如果一个协程因为某个连接读而变成 waiting 状态,这个连接有数据后,与之关联的协程就变成 ready,这个协程一直没去读数据,那水平触发一直就会 poll 出来该 fd,没必要。
Q: 内存什么时候释放?
内存释放分两步
没有存活对象的 span 被 GC 回收, 归还到 mheap 结构中,变成 free 的 page。
sysmon 协程会扫描,超过一段时间没有再被使用的 page(1.12 机制有改变), 通过 madvise 系统调用告诉操作系统,这些 page 对应的物理内存不再需要了,可以与虚拟内存解绑,给其他分配使用。
Q: 0.1+13+0.3ms 三个时间的意思?
GCDEBUG=gctrace=1 会打印出 gc 相关的时间,这三个分别代表,gc 开始时第一个 stw 的 wall time, 并发标记的 wall time 以及 GC 标记结束阶段 stw 的 wall time。
Q: []byte 于 string 的黑魔法
底层数据共享,减少数据拷贝。
https://jaycechant.info/2019/golang-unsafe-cast-between-string-and-bytes/
Q: 之前说的 netpoll,被 gopark 挂起的 G 扔哪了,怎么找到对应的 G,然后又怎么扔给对应的 M 的 runQ 的?
并没有扔哪里去,也没放在哪个队列。
一个协程因为某个网络 fd 的操作阻塞时,会把该 fd 添加到 epoll 中,使用以下系统调用。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};go 在 epoll_event 中的 epoll_data_t 放了一个指针值,该指针指向一个包含 runtime.g 的结构体。
下次 epoll_wait 时,便可把该 epoll_data_t 也 poll 出来,相当于与该 fd 关联的上下文,也就可以找到阻塞其上的协程。
不需要再放回对应的 M 的 runq 中,目前是通过 injectglist 放在全局的 runq 中。
观看视频
{{< youtube id="oFJL8S1dwsw" >}}
Night 65 2019 10 31 Go Net (`content/night/65-2019-10-31-go-net.md`)
desc: Go 夜读之 Go 网络编程:Go 原生同步网络模型解析 vs Multi-Reactors 异步网络模型 title: 第 65 期 Go 原生网络模型 vs 异步 Reactor 模型 date: 2019-10-31T21:00:00+08:00 author: 潘建锋@亚马逊
Go 夜读第 65 期 Go 原生网络模型 vs 异步 Reactor 模型
本期 Go 夜读是由 Go 夜读 SIG 核心小组邀请到潘建锋给大家分享 Go 原生网络模型 vs 异步 Reactor 模型,以下是本次分享的部分内容和 QA 。
引子
我们都知道 Golang 基于 goroutine 构建了一个简洁而优秀的原生网络模型,让开发者能够用同步的模式去编写异步的逻辑:goroutine-per-connection 模式,极大地降低了开发者编写网络应用时的心智负担,而且借助于 Go Scheduler 对 goroutines 的高效调度,这个原生网络模型足以应对绝大部分的应用场景。
然而,在工程性上能做到如此高的普适和兼容,给开发者提供如此简单易用的接口,其背后必然是基于非常复杂的封装,做了很多取舍,放弃了一些『极致』的概念和设计。事实上 Golang 的 netpoll 底层就是基于 epoll/kqueue/iocp 这些系统调用来做封装的,最终暴露出 goroutine-per-connection 这样的网络编程模式给开发者。
在绝大部分应用场景下,我推荐大家还是遵循 Golang 的 best practices,以这种模式来构建自己的网络应用,然而,在某些极度需要提高性能、节省资源以及技术栈必须是原生 Go (不考虑 C/C++ 写中间层供 Go 调用)的场景下,我们可以考虑自己构建 Reactor 网络模型。那么,Reactor 模型相对原生模型有哪些优势和弊端呢?我开发了的一个基于事件驱动机制的实验性质的异步网络框架:gnet,其在性能和资源占用上都远超 Go 原生 net 包(少数特定的应用场景),通过解析这个框架和 Go 原生网络模型,我们来一一分析~~
预备知识:epoll、非阻塞IO、IO多路复用, Linux IO模式及 select、poll、epoll详解
分享 Slides
Q&A 总结
Q1: 为什么 gnet 会比 Go 原生的 net 包更快?
答:
Multi-Reactors 模型相较于 Go 原生模型在以下场景具有性能优势:
- 高频创建新连接:我们从源码里可以知道 Go 模式下所有事件都是在一个 epoll 实例来管理的,接收新连接和 IO 读写;而在 Reactors 模式下,accept 新连接和 IO 读写分离,它们在各自独立的 goroutines 里用自己的 epoll 实例来管理网络事件。
- 海量网络连接:Go net 处理网络请求的模式是 goroutine per connection,甚至是 multiple goroutines per connection,而 gnet 一般使用与机器 CPU 核心数相同的 goroutines 来处理网络请求,所以在海量网络连接的场景下 gnet 更节省系统资源,进而提高性能。
- 时间窗口内连接总数大而活跃连接数少:这种场景下,Go 原生网络模型因为 goroutine per connection 模式,依然需要维持大量的 goroutines 去等待 IO 事件(保持 1:1 的关系),Go scheduler 对大量 idle goroutines 的调度势必会损耗系统整体性能;而 gnet 模式下需要维护的仅仅是与 CPU 核心数相同的 goroutines,而且得益于 Reactors 模型和 epoll/kqueue,可以确保每个 goroutine 在大多数时间里都是在处理活跃连接。
- 短连接场景:gnet 内部维护了一个内存池,在短连接这种场景下,可以大量复用内存,进一步节省资源和提高性能。
Q2: Go netpoll 源码里的 waitRead 方法到底是起什么作用?
答:
看源码:
// func (fd *FD) Read(p []byte) (int, error)
for {
n, err := syscall.Read(fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
// On MacOS we can see EINTR here if the user
// pressed ^Z. See issue #22838.
if runtime.GOOS == "darwin" && err == syscall.EINTR {
continue
}
}
...
// func netpollblock(pd *pollDesc, mode int32, waitio bool) bool
// need to recheck error states after setting gpp to WAIT
// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
if waitio || netpollcheckerr(pd, mode) == 0 {
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
}通过分析 conn.Read(),我们知道这个方法是同步的,但从源码我们可以看出,Go 使用的是非阻塞 IO,所以调用 syscall.Read 的时候并不会阻塞,所以实际上它是通过 waitRead 这个方法来实现阻塞的:netFD 的 Read 操作在系统调用Read后,当遇到 syscall.EAGAIN 时,waitRead 里面的 netpollblock 会调用 gopark 将当前读这个网络描述符的 goroutine 给 park 住,直到这个网络描述符上的读事件再次发生为止,唤醒 goroutine,waitRead 调用返回,回到外层的 for 循环继续执行。conn.Write 方法和 Read 的实现原理是一样的,都是在发生syscall.EAGAIN 错误的时候将当前 goroutine 给 park 住直到 socket 再次可写为止。
Q3: Go 的网络模型有『惊群效应』吗?
答:没有。
我们看下源码里是怎么初始化 listener 的 epoll 示例的:
var serverInit sync.Once
func (pd *pollDesc) init(fd *FD) error {
serverInit.Do(runtime_pollServerInit)
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
if errno != 0 {
if ctx != 0 {
runtime_pollUnblock(ctx)
runtime_pollClose(ctx)
}
return syscall.Errno(errno)
}
pd.runtimeCtx = ctx
return nil
}这里用了 sync.Once 来确保初始化一次 epoll 实例,这就表示一个 listener 只持有一个 epoll 实例来管理网络连接,既然只有一个 epoll 实例,当然就不存在『惊群效应』了。
Q4: Multiple Reactors + Goroutine-Pool Model 这个模式,把阻塞的任务放入Goroutine-Pool,但是如果 Response 依赖于阻塞任务返回的结果(比如依赖于一个http请求结果),这种情况Goroutine-Pool 是不是意义不大了?
gnet 提供了异步写的 API: AsyncWrite,一般都是在 goroutine pool 里处理完阻塞逻辑之后直接调用这个方法把 response 写回 socket,总之,原则就是不能阻塞 eventloop goroutine,也就是 gnet.React 方法。
Q5:
- 潘少说go-net原生的网络模型相当于单reactor的模型,每一个连接一个goroutine来处理,由go的调度器实现高并发,这样应该也能利用上多核CPU的吧?为什么性能比multi-reactors的方式差这么多?
- multi-reactors的主reactor万一挂了,怎么办?(类似单点故障问题)
- multi-reactors + goroutine pool的模式下,subreactor负责输入输出,goroutine pool负责计算,若某个任务的数据量比较大,从subreactor到goroutine pool或从goroutine pool到subreactor的数据传输成本会不会很大?
答:
问题 1 参见上面第一个我回答的问题;问题 2 说的 Reactor 单点问题的确是存在的,因为 gnet 使用的是主从 Reactors 模式,main reactor 只有一个,所以的确存在这个潜在的问题,解决办法也有:使用多 acceptors,利用 SO_REUSEPORT 参数让内核帮你做 load balancing 避免惊群;至于问题 3 :并不存在数据传输成本,从当前 eventloop goroutine 也就是 gnet.React 方法里一般是用 closure 闭包的方式提交任务到 goroutine pool 的,是引用方式。
Q6: goroutine pool如何把数据送回到subreactor?
当你在独立的 goroutine 里完成你的阻塞逻辑之后得到了 response 数据,直接调用 AsyncWrite:
func (c *conn) AsyncWrite(buf []byte) {
if encodedBuf, err := c.loop.svr.codec.Encode(buf); err == nil {
_ = c.loop.poller.Trigger(func() error {
if c.opened {
c.write(encodedBuf)
}
return nil
})
}
}通过 closure 的方式,写一个唤醒事件到 epoll,同时传一个 func() error 到任务队列,在 sub reactor 的那个 goroutine 里执行这个函数,把数据写回 client。
Q7: 在等待传回的这段时间,subreactor是不是还是得阻塞着,无法处理其他请求
不会阻塞啊,此时 React 方法已经返回了,你的阻塞逻辑是提交到 goroutine pool 里处理,处理完直接调用 AsyncWrite 异步写回去了,方式就是我上面说的,写一个唤醒事件到 epoll,在 eventloop goroutine 里执行,所以不会有同步问题。
回看视频
参考资料
- A Million WebSockets and Go
- Going Infinite, handling 1M websockets connections in Go
- 百万 Go TCP 连接的思考: epoll方式减少资源占用
- gnet: 一个轻量级且高性能的 Golang 网络库
观看视频
{{< youtube id="4QurJJHuxaQ" >}}
Night 66 2019 11 07 Paper Reading Csp (`content/night/66-2019-11-07-paper-reading-csp.md`)
desc: Go 夜读之 Paper Reading CSP 理解顺序进程间通信 title: 第 66 期 Paper Reading CSP 理解顺序进程间通信 date: 2019-11-07T21:00:00+08:00 author: 欧长坤@慕尼黑大学
Go 夜读第 66 期 Paper Reading CSP 理解顺序进程间通信
本期 Go 夜读是由 Go 夜读 SIG 核心小组成员欧长坤给大家带来的经典论文 CSP 的 Paper Reading。
CSP 是什么?
我们常常在讨论中提及 CSP,但鲜有人能真正说清楚 CSP 的演进历史,及其最核心的基本思想。我们已经对 Go 提供的并发原语足够熟悉了,是时候深入理解其背后的基础理论 —— 顺序进程间通信(Communicating Sequential Processes, CSP)了。本次分享我们针对 [Hoare 1978] 探讨 CSP 理论的原始设计(CSP 1978),主要围绕以下几个问题展开:
Tony Hoare 提出 CSP 的时代背景是什么?
- CSP 1978 理论到底有哪些值得我们研究的地方?
- CSP 1978 理论是否真的就是我们目前熟知的基于通道的同步方式?
- CSP 1978 理论的早期设计存在什么样的缺陷?
大纲
- CSP 1978 的诞生背景
- CSP 1978 的主要内容及其结论
- CSP 1978 理论中存在的设计缺陷
- 讨论与反思
分享 Slides
- https://docs.google.com/presentation/d/1N5skL6vR9Wxk-I82AYs3dlsOsJkUAGJCsb5NGpXWpqo/edit?usp=sharing
回看视频
参考资料
- [Hoare 1978] Hoare, C. A. R. (1978). Communicating sequential processes. Communications of the ACM, 21(8), 666–677.
- [Ou 2019a] CSP1978 的 Go 语言实现
- [Ou 2019b] 第 56 期 channel & select 源码分析
- [Ou 2019c] 第 59 期 Real-world Go Concurrency Bugs
观看视频
{{< youtube id="Z8ZpWVuEx8c" >}}
Night 67 2019 11 14 Sql Pool Reading (`content/night/67-2019-11-14-sql-pool-reading.md`)
desc: Go 夜读之 Go database/sql 数据库连接池分析 title: 第 67 期 Go database/sql 数据库连接池分析 date: 2019-11-14T21:00:00+08:00 author: 邹文通@POP
Go 夜读第 67 期 Go database/sql 数据库连接池分析
本期 Go 夜读是由 POP 后端团队的邹文通给大家带来的 Go 标准包 database/sql 数据库连接池源码剖析。
大纲
- sql 连接池简介
- 连接池的工作原理
- sql 包连接池源码分析
- 连接池使用 tips
Slides
- https://docs.google.com/presentation/d/10kGjeHGbB0h0Cz8f58reXOyCdyWSOSKrr2160IFNla4/edit?usp=sharing
回看视频
QA
1. database/sql 中 MaxIdleConns 和 MaxOpenConns 应该怎么设置才是相对合理的,在选择设置具体的值时,他们又受什么因素影响呢?
- 关于这个问题,可以参考这篇文章 Production-ready Database Connection Pooling in Go. 文章的建议是 MaxOpenConns 应该和实际的打开的连接数的监测值相关。然后按照 MaxOpenConns 的一定比值设置 MaxIdleConns,比方说 50%,这个值取决于你对业务的预估。每维持一个闲散连接,会造成 1MB 左右的客户端内存开销和 2MB 左右的数据库内存开销,CPU 开销相对小一点。文章还给出了一些 benchmark 的测试,在默认 MaxIdleConns 和 MaxIdleConns = 50% * MaxOpenConns 情况下的一个性能的对比,可以参考一下。
参考资料
- Go组件学习——database/sql数据库连接池你用对了吗
- Go组件学习——手写连接池并没有那么简单
- Chapter 8 Connection Pooling with Connector/J
- 彻底弄懂mysql(二)--连接方式
观看视频
{{< youtube id="JKJ8ehtiqUM" >}}
Night 68 2019 11 21 Dive Into Network (`content/night/68-2019-11-21-dive-into-network.md`)
desc: Go 夜读之网络知识十全大补丸 title: 第 68 期网络知识十全大补丸 date: 2019-11-21T21:00:00+08:00 author: 刘楠
Go 夜读第 68 期网络知识十全大补丸
本期 Go 夜读是由刘楠给大家带来的『网络知识十全大补丸』。
作者介绍
刘楠 (NandyLiu),曾经的少年黑客,大学时的计算机学院网管,毕业后曾在某前沿网络设备厂商研发网络安全和流控产品,解决了行业性的网络流控难题,在中美两国有流控算法专利,Network traffic control method and device(CN 102355425, US 8,804,525 B2). 后又在多个行业的头部企业任职,为亿级在线的国民聊天软件优化网络调度和传输体验,为金融市场开发高性能高实时性的金融行情系统。出身于教师世家,生活成长于校园,乐于传道和分享自己的想法。
背景
后端工程师在工作中经常会遇到计算机网络方面的问题,网络对多数人来说还是一个黑盒子,本次技术分享从常见的网络硬件、企业和数据中心的网络拓扑、Linux 协议栈和防火墙等基础网络知识开始介绍,一直讲到 TCP 和 HTTP 这些年的技术演进路线和未来趋势。
大纲
- 网络硬件介绍
- 网络部署模式
- Linux 协议栈
- Linux 防火墙
- 前沿网络传输技术
QA
1. tap 是虚拟网卡吗?
虚拟网卡有很多种,比如 loopback 的 lo,docker 容器的 veth,tap 也是其中一种。
2. https 也能检测到数据包吗?
https 因为有加密所以一般不能检测数据包,强行检测需要使用中间人攻击伪造证书,会被客户端发现。
3. 老师想问下,跨网通讯,目的 mac 地址只填网关 mac 地址就可以了么,然后让网关自己去跳?
发往同网段的话通过 arp 或路由表填充目的 mac 地址,那么发送跨网段数据包-目的 mac 地址不需要填直接发给网关么,让网关来跳
本机通过路由,把跨网段的数据包在二层发给网关,所以目的MAC地址是网关的MAC地址,网关收到这个数据包后进入自己的协议栈的第三层,查找路由转发给下一跳的节点。
4. mac 地址不是随着数据包换的吗?
MAC 地址是二层网络使用的地址,二层网络属于一个邻居系统,只支持邻居之间的通信,所以 MAC 地址在二层网络中转发时不变,跨越二层经过三层设备的时候就会改变。
5. 网关与网桥感觉很像?
网关工作于三层,网桥工作于二层,网关是内网和外网之间的三层路由节点,网桥将两个二层网络连接为一个更大的二层网络。
6. 我觉得 VPN 和电路层代理很像,大佬可以讲讲?
VPN 工作于三层或以上,电路层代理是什么概念?是想问 socket 代理吗?四层的 SSL VPN 跟 socket 代理从概念到使用的技术都非常类似。
7. 请问一个问题,最近发现集群的网卡 drop 包很多,我们监控了一下平均每分钟都有几个,会有哪些问题导致的呢?驱动会不会影响这个参数?
drop 的原因很多,流量太大,负载过高,网卡的 buffer 满了,就可能会 drop,可以通过降低流量来观察 drop 会不会减少并消失。
8. 混杂模式下抓也会漏包
一般不会漏,不过上层的交换机如果只把属于抓包的这台机器的数据包转发过来,不属于你的数据你就抓不到了。如果想抓整个局域网的数据包,要使用ARP欺骗技术,把自己伪装成局域网的网关,实现中间人攻击。
- 看一下上层是不是收不过来了
- mtu?
有 mtu 问题的可能性,不过也不是太大。
9. 软中断就是 汇编中的 int 吧
中断下半部
Int 就是一个中断指令吧
软中断这个概念是用来处理中断任务的下半部(bottom half),简化硬件中断程序的处理逻辑,让硬中断程序只处理简单的 IO 操作,之后复杂的逻辑留给软中断程序来处理,减少硬中断占用 CPU 的时间,提高 CPU 对其他中断的处理实时性,简单的说就是一个任务分成两部分,上半部分是紧急处理阶段,下半部分是非紧急处理阶段,下半部分在具体实现上可以使用软中断的模式,软中断可以用 int 指令来主动触发,从而让 CPU 来处理下半部的逻辑。
10. I/O 的 select 还是 poll 模式,是在哪个环节有区别呢?
select 和 poll 用来操作 socket,是协议栈传输层以上的处理逻辑,用于轮询 socket,检查轮询的一批 socket 的发送和接受缓冲区是否可读可写。
11. 老师对 dpdk 了解吗 一直不知道处于哪个步骤
dpdk 技术在网卡驱动程序中就已经介入,网卡收发数据包直接使用 dpdk 设定好的内存区域,这个内存区域再映射到应用层,实现应用层直接与网卡驱动的数据交换,绕过操作系统协议栈,实现高性能网络数据处理
cpu 绑定?
硬直通 ovs evs 都可以绑定 cpu
可以通过二进制掩码的形式,实现 IRQ Affinity,指定中断由哪些 CPU 来处理,也叫中断的 CPU 绑定
12. local_in 就是 iptables 的 input 链吧
是的
13. iptables 可以用来做限速吗?比如模拟丢包,弱网
tc
tc 命令用于在二层构建数据发送队列,支持队列的各种调度,比如增加延时,设定丢包的概率模型,可以模拟常见的网络场景,实际上很多广域网模拟器的硬件,就是通过 linux + tc 在一个嵌入式的小盒子里实现的。
14. mangle 表一般可以做一些什么事情
用于对数据包进行修改,比如修改 IP 包头的 TTL 字段
15. 劫持是用 mangle 吗?
mangle 通常只对单个包做简单的修改,复杂的劫持逻辑不方便在内核中实现,一般会在 nat 中做一个 dnat,将需要劫持的流量的目的 IP 定向到本机,然后在本机监听对应的端口,实现中间人或代理劫持。
16. 那个经典了计算机网络的绿皮书讲 tcp 很不错
17.
https://www.7down.com/soft/179692.html
18. bbr 一般用于 1080 优化?
bbr 可以抗丢包,在有丢包的情况下依然能保持大带宽,所以对于 1080p 或更高清的视频传输,也是有很好的效果的。
19. 我在公司让 sa 帮忙把一台机器设置了 BBR,升级了内核,但是用 iperf 测试效果不是很好,一直没算弄清楚原因
sysctl -w net.ipv4.tcp_congestion_control=bbr
先要检查看你内核使用的是不是 BBR 算法,另外 BBR 只对发送端有效果,如果你开启BBR的是接收端,发送端没有开启 BBR,是没有意义的。BBR 最大的优势在于抗丢包,如果网络中没有随机丢包(非拥塞导致的丢包),BBR 相对传统 TCP 算法并没有明显优势。
20. 有比 BBR 更优秀的?
拥塞控制算法有自己的适用场景,另外是否优秀也有很多评价标准,有人认为 BBR 还不够激进,需要提高侵略性,也有人认为 BBR 抢占其他算法的带宽不公平。
21. 插个问题,我们用 1.1,但是也用了 WSS,这个时候 WSS 不知道是握手还是连接超时,到了六秒中,这个时候同一个页面其他的请求都没有收到,这个应该是 1.1 中阻塞了吧
我们用 1.1,但是也用了 WSS,这个时候 WSS 不知道是握手还是连接超时,到了六秒中,这个时候同一个页面其他的请求都没有收到,这个应该是 1.1 中阻塞了吧?建立连接的时候 WS 还是 HTTP 协议,只不过走 upgrade 升级,服务端收到 upgrade 后切换协议,大概这样的,WS 刚刚通过 HTTP 传的时候会不会新建 TCP?
不一定 ws会和页面其他请求用不同的 tcp 连接
websocket 会一直使用同一条 TCP 连接,你说的超时是晚到还是一直没有到?如果是一直不到,可能中间的链路对 websocket 有干扰,比如企业网出口的 http 代理,有可能把 websocket 当做普通 http 请求来处理,不支持 websocket 的 upgrade 操作,建议排查客户端的网络环境,比如让客户端通过手机开热点上网,对照测试,用排除法排查。
FEC也是一种纠错算法
那 fec 包也丢了咋办?
FEC 不是正常的业务包,丢了不影响业务的传输,没有关系。加入多少比例的 FEC 要看实际丢包率,FEC 要有足够的比例来填丢包的坑。
22. 老师给我们推荐点相关书籍吧
见参考资料。
回看视频
参考资料
- W・Richard Stevens, TCP/IP 详解 卷 1:协议,TCP/IP ILLustrated Volume 1: The Protocols, 1994
- Christian Benvenuti, 深入理解 LINUX 网络技术内幕,Understanding Linux Network Internals, 2005
- Gnv Vibhav Reddy, G.Vijay Kumar, L.Roshini, Congestions and Control Mechanisms in Wired and Wireless Networks, 2014
- Neal Cardwell, Yuchung Cheng, BBR: Congestion-Based Congestion Control, acmqueue, 2016
- QUIC: a multiplexed stream transport over UDP, https://www.chromium.org/quic
- HTTP/3: draft-ietf-quic-http-latest, https://quicwg.org/base-drafts/draft-ietf-quic-http.html
观看视频
{{< youtube id="30wCahZEjNg" >}}
Night 69 2019 11 28 Devops (`content/night/69-2019-11-28-devops.md`)
desc: Go 夜读之基于 Go 语言周边生态打造的行业技术中台 title: 第 69 期 DevOps 实践之路 - 基于 Go 语言周边生态打造的行业技术中台 date: 2019-11-28T21:00:00+08:00 author: 杨晖@腾讯教育
Go 夜读第 69 期基于 Go 语言周边生态打造的行业技术中台
本期 Go 夜读是由杨晖给大家带来的『基于 Go 语言周边生态打造的行业技术中台』。
作者介绍
杨晖,腾讯云,教育行业技术负责人,打造教育行业技术解决方案,提供统一的对外技术中台服务。
大纲
Go 语言生态在腾讯云教育行业中的运用和实践
- 腾讯教育介绍
- 建设思路
- 详细设计
- 实践
- 总结 & 后续优化
回看视频
分享内容 PPT
https://github.com/talkgo/night/issues/487
观看视频
{{< youtube id="wS897lDB_Lk" >}}
Night 7 2018 05 24 Net Http Part1 (`content/night/7-2018-05-24-net-http-part1.md`)
title: 第 7 期 2018-05-24 线下活动 - Go 标准包阅读 date: 2018-05-24T11:49:10+08:00
参与人数: 10 人
Go 标准包阅读
Go 版本:go 1.10.1
net/http
- server.go
问题
- Next Protocol Negotiation = NPN
- Expect 100 Continue support
见参考资料
- header提到了:Expect和host
- 判断了 header里面的HOST,但是后面又删除,为什么?
server.go#L980
delete(req.Header, "Host")- 判断是否支持 HTTP2 (isH2Upgrade)
// isH2Upgrade reports whether r represents the http2 "client preface"
// magic string.
func (r *Request) isH2Upgrade() bool {
return r.Method == "PRI" && len(r.Header) == 0 && r.URL.Path == "*" && r.Proto == "HTTP/2.0"
}调用:ProtoAtLeast(1, 1)
...
// ProtoAtLeast reports whether the HTTP protocol used
// in the request is at least major.minor.
func (r *Request) ProtoAtLeast(major, minor int) bool {
return r.ProtoMajor > major ||
r.ProtoMajor == major && r.ProtoMinor >= minor
}会议讨论
20:07:45 From 永京 李 : ok
20:07:46 From 斯 艾 : 可以
20:07:50 From joe sean : ok
20:07:51 From 洪范 郝 : 可以
20:07:54 From 力宁 关 : 可以
20:08:01 From 洪范 郝 : 可以
20:08:05 From 力宁 关 : ok
20:08:09 From joe sean : 有杂音
20:08:46 From 斯 艾 : 有声音
20:08:51 From joe sean : 有的
20:08:54 From Jayden Yang : 有
20:08:59 From 永京 李 : you
20:09:17 From Jayden Yang : 回声
20:09:17 From 洪范 郝 : 噪音太大
20:10:28 From caigaoxing : 2343
20:12:43 From 洪范 郝 : 屏幕卡了?
20:12:49 From joe sean : 1
20:12:53 From Jayden Yang : 1
20:13:57 From 迪 麦 : 屏幕是卡主了
20:14:26 From joe sean : 能看到鼠标动,看不到屏幕动
20:16:18 From caigaoxing : ok
20:17:25 From caigaoxing : 是不是不动了
20:17:45 From 和宽 熊 : 动了
20:17:47 From joe sean : activeConn是连接池吧
20:19:03 From joe sean : 又卡了
20:19:43 From 斯 艾 : 屏幕不动了
20:20:38 From kas li : 不行哦, 卡屏,只有鼠标能显示
20:20:43 From caigaoxing : 1
20:20:44 From caigaoxing : 1
20:20:44 From caigaoxing : 1
20:20:44 From caigaoxing : 1
20:20:44 From caigaoxing : 1
20:20:45 From caigaoxing : 1
20:20:45 From caigaoxing : 1
20:20:45 From caigaoxing : 1
20:20:45 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:46 From caigaoxing : 1
20:20:47 From caigaoxing : 1
20:20:48 From caigaoxing : 1
20:20:51 From caigaoxing : 卡了
20:20:55 From caigaoxing : 卡了
20:20:58 From joe sean : 1
20:21:04 From 建雷 张 : hi
20:21:07 From kas li : 卡屏,只有鼠标能显示
20:21:08 From caigaoxing : 不动了屏幕
20:21:09 From 叶 思杰 : 只能看到屏幕
20:21:14 From 叶 思杰 : 只能看到鼠标
20:21:19 From joe sean : ok
20:21:28 From brile ho : ok
20:21:31 From 叶 思杰 : ok
20:22:18 From kas li : 多余的视频关了,减少带宽使用
20:25:52 From caigaoxing : ok
20:25:56 From joe sean : 可以
20:26:01 From 夜 暗 : 先整体,再细节?
20:26:02 From 龙 周 : ok
20:26:04 From kas li : 跳进去有思路,也是不错的
20:26:31 From yi zhang : 听到声音了
20:29:20 From 洪范 郝 : setupHTTP2_Serve 这命名不规范呀
20:30:09 From DW : 跟踪 listener 主要是做什么的?
20:30:59 From ksir : 用goland不是很爽?
20:31:09 From yi zhang : 用goland啊
20:31:12 From Jayden Yang : goland呢
20:31:19 From Jayden Yang : vscode呢
20:31:27 From DW : sublime 跳转也很好用
20:32:05 From 建雷 张 : emacs
20:32:53 From mai yang : 现在直播正常了吧。
20:33:07 From DW : 临时错误
20:36:18 From DW : 我还以为是动然规划算法 。。。
20:36:43 From 永京 李 : backoff 算法设置重试间隔
20:37:40 From DW : 协程数量有没有限制的?
20:37:54 From 洪范 郝 : 可以
20:39:08 From Jayden Yang : 直接深入吧
20:39:12 From yi zhang : srv.trackListener(l, true)
defer srv.trackListener(l, false)
20:39:21 From yi zhang : 这两个函数是在干嘛啊?
20:39:46 From DW : 每连接一个连接,就 go 一个,会不会出现资源耗尽?
20:42:38 From yi zhang : srv.trackListener(l, true)
defer srv.trackListener(l, false)
这两个函数是在干嘛啊?
20:43:29 From yi zhang : 嗯 好
20:43:37 From yi zhang : 谢谢
20:44:14 From 洪范 郝 : 代码为啥不一样呢
20:44:41 From 大 猫 : @DW 我感觉创建上百万个协程没事,而且一个连接处理完了,资源就会回收
20:47:02 From mai yang : go1.10
20:47:05 From DW : 也就是说还没有一个安全机制进行控制是吧 @大猫
20:49:54 From 大 猫 : 有没有机制我也不知道,如果系统实在没法继续处理的话,应该表现的会卡,然后超时
20:50:45 From DW : 单机应该支持不了那么多的
20:51:12 From jinleileiking : 提问的人说的话听不清楚
20:51:19 From DW : 这里的 context 主要作用是什么?
20:52:18 From yi zhang : 控制其他goroutine的 取消、停止 等一些操作吧
20:53:05 From DW : 嗯,对应该是与其他 goroutine 交互的
20:54:08 From yi zhang : 哈哈 真的应该早点用用goland,没法跟踪函数,看interface的实现,简直就没法看代码啊
20:54:44 From jinleileiking : vim-go 跳转无压力
20:56:54 From DW : vs 无压力
20:58:51 From yi zhang : infinite
20:58:58 From yi zhang : 时间吧
20:59:01 From jinleileiking : { cr.remain = maxInt64 }
20:59:03 From yi zhang : 无穷
21:01:12 From yi zhang : 950
21:01:17 From yi zhang : :950
21:04:22 From DW : 主次版本
21:04:29 From Jayden Yang : && 优先级高么
21:04:46 From cym : major.minor
21:04:47 From DW : || 最高
21:08:36 From yi zhang : // ValidHostHeader reports whether h is a valid host header.
func ValidHostHeader(h string) bool {
// The latest spec is actually this:
//
// http://tools.ietf.org/html/rfc7230#section-5.4
// Host = uri-host [ ":" port ]
//
// Where uri-host is:
// http://tools.ietf.org/html/rfc3986#section-3.2.2
//
// But we're going to be much more lenient for now and just
// search for any byte that's not a valid byte in any of those
// expressions.
for i := 0; i < len(h); i++ {
if !validHostByte[h[i]] {
return false
}
}
return true
}
21:09:43 From 建雷 张 : godef: no declaration found for httplex.ValidHostHeader
21:09:57 From Jayden Yang : httplex.go
21:10:29 From jinleileiking : - var validHostByte = [256]bool{
| '0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true,
| '8': true, '9': true,
|
| 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true,
21:10:55 From Jayden Yang : /usr/local/go/src/vendor/golang_org/x/net/lex/httplex/httplex.go
21:12:06 From jinleileiking : [256]bool 下标是 ‘0’
21:13:41 From 协 崔 : if deleteHostHeader {
delete(req.Header, "Host")
}卧槽,我看到的是这样的
21:14:40 From joe sean : 1
21:15:17 From mai yang : go1.10.1
21:15:20 From Jayden Yang : deleteHostHeader 这个又是啥
21:15:22 From mai yang : 我们基于这个版本的。
21:16:03 From Jayden Yang : // whether Close should stop early
21:19:15 From 协 崔 : 有个 叫chunked的编码
21:28:40 From 马嘉 : 再来把
21:29:25 From 建雷 张 : ...
21:29:28 From mai yang to Henry Lee (Privately) : 刚刚网络问题中断了。现在继续。
21:29:38 From mai yang : 刚刚网络问题中断了。现在继续。
21:29:45 From 大 猫 : 屏幕没共享
21:30:54 From 马嘉 : ok
21:31:01 From 建雷 张 : ok
21:32:19 From Jayden Yang : 你们只能开一个
21:32:24 From Jayden Yang : 现场只开一个
21:32:34 From Jayden Yang : 音中音了
21:38:52 From DW : 什么样的客户端会使用 100-continue 协议呢?
21:39:21 From Core Code : 这个是询问服务端是否支持大的包吧
21:39:48 From DW : 浏览器是不是自动实现了这种协议
21:52:15 From 洪范 郝 : // Buffered returns the number of bytes that can be read from the current buffer.
func (b *Reader) Buffered() int { return b.w - b.r }
21:57:59 From Jayden Yang : closeNotifyCh is the channel returned by CloseNotify.
21:59:06 From Jayden Yang : 发送关闭信号给所有的goroutune
21:59:41 From Jayden Yang : // closeNotifyCh是由CloseNotify返回的频道。
// TODO(bradfitz):目前(对于Go 1.8)总是这样
//非零。 让这个懒洋洋地再创造一次,因为它曾经是?
22:12:04 From Jayden Yang : 看不到屏幕
22:12:37 From jinleileiking : 看不到屏幕
延伸阅读
- https://github.com/golang/go/issues/22128
- https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-26#section-6.2.1
- https://www.cnblogs.com/tekkaman/archive/2013/04/03/2997781.html
- https://benramsey.com/blog/2008/04/http-status-100-continue/
- http://www.ituring.com.cn/article/130844
观看视频
{{< youtube id="H3oXjpiOReQ" >}}
Night 70 2019 12 05 Go Details (`content/night/70-2019-12-05-go-details.md`)
desc: Go 夜读之 Go 中不常注意的各种细节集锦 title: 第 70 期 Go 中不常注意的各种细节集锦 date: 2019-12-05T21:00:00+08:00 author: 老貘@Golang101作者
Go 夜读第 70 期 Go 中不常注意的各种细节集锦
本期 Go 夜读是由老貘给大家带来的『Go 中不常注意的各种细节集锦』。
作者介绍
老貘,《Golang 101 语言》一书的作者。
背景
Go 语言简单易上手,很多 Go 程序员用了几年 Go 之后,常会觉得对 Go 的各方面已经了如指掌。
但是我发现实践中大家常常对 Go 中的一些细节并不了解。虽然这并不妨碍使用 Go,但是知道这些细节会对大家更好更职业地使用 Go 带来帮助。
大纲
- Go 中的断行规则导致的一些反直觉例子
- Go 中一些和类型推断相关的细节。
- 各种场景下的表达式估值顺序
- might be more
回看视频
分享内容 PPT
https://github.com/talkgo/night/issues/521
观看视频
{{< youtube id="kcwHwN8nTP8" >}}
Night 71 2019 12 12 Go Ini (`content/night/71-2019-12-12-go-ini.md`)
desc: Go 夜读之 go-ini 配置库评析 title: 第 71 期 go-ini 配置库评析 date: 2019-12-12T21:00:00+08:00 author: 无闻
【Go 夜读】#71 go-ini 配置库评析
go-ini 作为目前 Go 语言最流行的 INI 解析库,有哪些特点、有哪些不足,开发的初衷和未来。
大纲
- 开发初衷
- 与市面上其它库的比较
- 有哪些特点和不足
- 设计思路
- 性能对比
- 后续功能展望
分享者自我介绍
无闻,目前就职于初创企业 Sourcegraph,软件工程师,没什么特长,就是啥都要干一点。
分享时间
2019-12-12 21:00 UTC+8
分享地址
Slides
https://docs.google.com/presentation/d/1ruDCybd9v7oWBx3WoIC-G26PjfnOdu3GwfSphED8UlM/edit?usp=sharing
参考资料
观看视频
{{< youtube id="783MGw53gfw" >}}
Night 72 2019 12 19 Go Micro 2 (`content/night/72-2019-12-19-go-micro-2.md`)
desc: Go 夜读之 Go-Micro 编写微服务实战 title: 第 72 期 Go-Micro 编写微服务实战 date: 2019-12-19T21:00:00+08:00 author: 舒先
【Go 夜读】#72 Go-Micro 编写微服务实战
往期:第一期
基于Go-Micro开发、实现如下服务架构:
包含Go-Micro核心功能
大纲
设计中,大致如下:
- 使用Go-Micro编写微服务
- 示例架构,使用微服务进行两个数学计算并返回结果
- Go-Micro中的服务类型:Srv、Web、API
- Broker与异步消息
- Wrapper与日志、限流
- 如何使用插件:示例使用RabbitMQ插件
- Micro API网关浅讲
分享者自我介绍
Micro 中国团队
陈洪波:本期资料提供与审核,10年,成都云创新技术经理,Gopher、Phper,全栈码农,专注于微服务架构&服务网格技术的应用。
Edward Chen:本期资料提供与审核,10年+,Honeywell HBT 研发技术专家,擅长Racher/K8S/Go/C#/C++/C/Typescript,专注于大规模IOT容器化微服务架构,一线开发。
舒先:本期资料提供与主讲,7年+金融级微服务开发经验,OPPO,Micro维护者团队成员,负责中国生态。
徐旭:本期资料提供与审核,应届,趣头条工程师,喜欢开源,Gev作者,擅长网络编程与表情包。
分享时间
2019-12-19 21:00:00 UTC+8
分享地址
Slides
参考资料
分享中提到的资料
问题
- 什么是 Function,其场景是什么?
答:Function 是 FaaS 服务中的一种服务类型,旨在提供只响应一次的服务,响应后该服务便不会再生存,会被卸载掉。场景比较我们有时接口是用来初始化数据的,那就可以用 Function,借用容器管理,可以重复发布 Function 类的服务,让其在有限的范围内提供服务。参考,代码示例
- 服务聚合的场景是什么?
答:这个是业务架构的设定,当我们把服务拆分成微服务时,各服务之间处理的能力基本是独立不耦合的,那我们要把它们结合起来提供能力对外服务,就需要服务聚合。比如常见的订单查询,通常会有查询用户订单与商品详情的需要,而用户服务与商品服务又是各自独立,这便是聚合服务存在的价值。它的本质是把服务能力按业务需要整合。
- 多个 Server 分仓库,proto 文件如何存放?
答:proto 文件本质是接口描述文件,推荐放在公共目录或公共仓库,定义后,转成不同语言的接口包,也存放在公共仓库,这样任何服务想用就去找就行了。
- http可以替换成其它框架吗?
答:可以,go-micro 的 web 包只是为大家包装了注册到 go-micro 生态的能力,其它使用的基本是 go 原生的 http 能力。如果想使用其它框架,自行定义 go-micro 的 client(client.NewClient(opts...))来请求其它 go-micro srv 服务即可。
- HttpTransport 为什么要改成 gRPC 的?
答:这个 Http 始终只适合放在C端的客户端,在RPC框架中使用,基于gRPC更好,总得来说,gRPC 是基于 2.0 的 HTTP 构建通信模式,它是专业的面向后台应用的通信协议。gRPC 优化了很多部分,比如一般我们用HTTP传送数据时,都是可读的 JSON 结构,而 gRPC 使用了效率更高的 protobuf 格式,HTTP 当然也可以使用 protobuf 的 Payload,但是 gRPC 已经做了,为什么我们要再重新做呢?gRPC 的缺点就是通信两端都要支持,否则没用,HTTP 则没有这个顾虑。go-micro 在这方面都是可插拔的,大家想用哪个 Transport,在命令行或 API 参数中使用即可。
观看视频
{{< youtube id="7lvZhhgUB7o" >}}
Night 73 2019 12 28 Qrpc (`content/night/73-2019-12-28-qrpc.md`)
desc: Go 夜读之趣头条在长链接方面的实践 - qrpc title: 第 73 期趣头条在长链接方面的实践 - qrpc date: 2019-12-28T21:00:00+08:00 author: 徐志强@趣头条
【Go 夜读】#73 趣头条在长链接方面的实践-qrpc
qrpc是一个小而强大的长链接框架,它包含了谷歌grpc的核心特性,但是更加轻量、高效、可扩展,从最早的推送中台长链接,到后来整个的IM中台,以及目前进行中的qfe长链接网关,正在朝着成为趣头条的长链接底座的方向发展。
大纲
1.为何qrpc
2.qrpc的特性和生态
3.qrpc的应用
4.qrpc核心的优化
5.qrpc的展望
6.pprof tips
分享者自我介绍
徐志强,开源爱好者,Cocos2dx、TiDB、GO contributor。
目前在趣头条的母公司比格基地担任架构师。
负责推送和IM中台,目前已经接入了多个日活500-800w的业务方。
研究兴趣点:网络+存储+高并发。
分享时间
2019-12-28 21:00 UTC+8
分享地址
Slides
在线版 qrpc.pptx 需翻墙
离线版 qrpc-夜读.pptx
备注
Q:不用epoll怎么保持80W连接?
A:其实go内部所有连接都已经用epoll(或者各平台对应物)接管了,自己再做集成的好处,是可以把闲置的协程释放掉
Q:IM优雅重启时老进程不退出的原因?
A:老进程关闭所有端口后做Shutdown,前面有个bug,导致无法退出(较低概率出现),相关的commit是这个:https://github.com/zhiqiangxu/qrpc/commit/951759ff4f03b4c34d76092f634817444bbc2d35
Q:3个协程如何优化成2个?
A:通过抢占锁的方式,抢到的一方再把其他的写请求接管过来,最后通过writev批量写,最终结果是内存和性能都有收获
Q:没有任何监听端口时的调试工具?
A:delve,谷歌官方推荐
Q:qrpc是基于http2还是tcp?
A:tcp,没有http2的历史包袱
Q:用qrpc写聊天工具是不是只要写ui就好了?
A:对,长链接部分会非常简单,不过由于IM涉及的接口繁多,还是需要开发很多的restful接口做存储
Q:串行、并行的区别
A:串行的话,前面的包处理完,后面的才会处理;并行的话,不会等前面的处理完就会开始执行
Q:TLS可以集成吗?
A:规划中,也可以使用TLS proxy分担出去。
Q:并行模式下,包的顺序如何保证?
A:并行模式下,包的顺序没保证,可能后面的请求先返回了,请求/响应是通过8字节的RequestID进行关联
Q:socket断了如何保证消息不丢?
A:IM的消息都会做存储,客户端是推拉结合,重连后会跟server端进行同步
Q:如何维持心跳,是否需要重发机制?
A:qrpc内部做了tcp层的心跳,业务层也可以做,通过周期地请求某个CMD即可实现。tcp本身已经保证了可靠性,所以基于tcp做上层应用不需要考虑重发,除非连接断开了,那么下次重连会同步。
Q:push的时候如何去重?
A:这个是客户端根据消息ID做的幂等处理
Q:路由怎么做?
A:一致性哈希或者路由表存起来,前者成本比较低,目前用一致性哈希
Q:目前遇到的主要挑战是?
A:正准备上聊天室,一天一亿条消息,并且聊天室里成员众多,实际写放大会很大,对长链会是个考验
Q:优雅重启时新老进程如何通信?
A:推荐 https://github.com/cloudflare/tableflip
观看视频
{{< youtube id="Ug4i4IHhGh4" >}}
Night 74 2020 01 02 Time In Go 1 14 (`content/night/74-2020-01-02-time-in-go-1-14.md`)
desc: Go 夜读之 time.Timer 源码分析 (Go 1.14) title: 第 74 期 time.Timer 源码分析 (Go 1.14) date: 2020-01-02T21:00:00+08:00 author: 欧长坤
【Go 夜读】#74 time.Timer 源码分析 (Go 1.14)
time 是一个很有意思的包,除去需要获取当前时间的 Now 这一平淡无奇、直接对系统调用进行封装( runtime.nanotime )的函数外,其中最有意思的莫过于它所提供的 Timer 和 Ticker 了。他们的实现,驱动了诸如 time.After, time.AfterFunc, time.Tick, time.Sleep 等方法。
即将发布的 Go 1.14 将为 Timer 及其相关依赖带来大幅性能,本次分享我们就来详细分析以下 Go
1.14 中 time.Timer 的源码及其演进过程。
大纲
- 调度器与调度循环
- Timer 状态机
- Timer 的启动、终止与重置
- Timer 的触发时机
- Go 1.10 以前以及 Go 1.10 的 Timer 实现
分享时间
2020-01-02 21:00 UTC+8
分享地址
Slides
参考资料
- runtime: improve timers scalability on multi-CPU systems
- runtime: make timers faster
- Go 夜读第 56 期:channel & select 源码分析
- Go 语言原本: 计时器
- timer-based task scheduler
观看视频
{{< youtube id="XJx0eTP-y9I" >}}
Night 75 2020 02 06 The State Of Go In 2020 (`content/night/75-2020-02-06-the-state-of-go-in-2020.md`)
desc: Go 夜读之 2020 年 Go 的一些发展计划(Go 1.14 && Go 1.15) title: 第 75 期 2020 年 Go 的一些发展计划(Go 1.14 && Go 1.15) date: 2020-02-06T21:00:00+08:00 author: 杨文
【Go 夜读】#75 2020 年 Go 的一些发展计划
聊聊即将发布的 go 1.14 都有哪些新特性和性能提升,然后再跟大家一起看看 go 1.15 的 Proposal。
最后我和大家一起谈谈你对 pkg.go.dev 的看法。
大纲
- go 1.14 介绍
- go 1.15
- pkg.go.dev
分享者自我介绍
杨文, POP
分享时间
2020-02-06 21:00:00 UTC+8
分享地址
Slides
https://docs.google.com/presentation/d/1cXXbr5i9P3JE9DjVDfnK9yxZWUwMxs-VWKgScXc3D80/edit?usp=sharing
参考资料
- https://golang.org/doc/go1.14
- https://blog.golang.org/pkg.go.dev-2020
- https://blog.golang.org/go1.15-proposals
- https://maiyang.me/post/2020-02-05-image-jpeg-decode-is-slow/
备注
针对此次分享的 QA 请分享者在分享之后,整理同步到此 issues 后面。
观看视频
{{< youtube id="hAshoKXMMu0" >}}
Night 76 2020 02 20 Kubernetes Scheduler Design (`content/night/76-2020-02-20-kubernetes-scheduler-design.md`)
desc: Go 夜读之 Kubernetes Scheduler 设计与实现 title: 第 76 期 Kubernetes Scheduler 设计与实现 date: 2020-02-20T21:00:00+08:00 author: Draven
【Go 夜读】#76 Kubernetes Scheduler 设计与实现
谈谈 Kubernetes 的架构设计以及 Kubernetes Scheduler 怎么把你的 Pod 调度到某个 Node 上的。
大纲
- Kubernetes 架构设计
- Kubernetes 调度器历史与设计演变
- 基于谓词与优先级的调度器
- 基于调度框架的调度器
- Kubernetes 调度器实现分析
- 番外篇(Optional)
- 反调度器
- 批处理调度器
分享者自我介绍
Draven,面向信仰编程 作者,Kubernetes 搬砖工,负责给 issue 贴 Label。
分享时间
2020-02-20 21:00:00 UTC+8 (真是个好日子)
Slides
https://docs.google.com/presentation/d/1zVftY8VhOfTqGYvQogFMUvB8WLTCIPJXUmeZnDOGzNE/edit?usp=sharing
参考资料
观看视频
{{< youtube id="1cQt2bXJtME" >}}
Night 77 2020 03 05 Reading Go Earnings (`content/night/77-2020-03-05-reading-go-earnings.md`)
desc: Go 夜读之阅读 Go 源码带来的收益 title: 第 77 期阅读 Go 源码带来的收益 date: 2020-03-05T21:00:00+08:00 author: 杨文
【Go 夜读】#77 阅读 Go 源码带来的收益
结合我最近阅读的 Go 源码,进而应用到开源项目,并反馈到我们项目的过程,来跟大家聊聊阅读 Go 源码带来的收益。
大纲
- 了解 Go 代码的设计与实现(学习)
- 了解 Go 的代码变更缘由(学习+知其所以然+应用实践)
- 了解 Go 团队的工作模式(更高效)
- 了解 Go 周边生态(丰富自己)
分享者自我介绍
杨文,POP,Go 夜读 SIG 小组成员
分享时间
2020-03-05 21:00:00 UTC+8
Slides
https://docs.google.com/presentation/d/1-OPi5xZRm-RSnpDrT-6SdGF6srccqejkhYJrxwf278Q/edit?usp=sharing
观看视频
{{< youtube id="cWSTLlE59rY" >}}
Night 78 2020 03 11 Go Scheduler Reading (`content/night/78-2020-03-11-go-scheduler-reading.md`)
desc: Go 夜读之 Go Schedular 源码阅读 title: 第 78 期 Go Schedular 源码阅读 date: 2020-03-11T21:00:00+08:00 author: 饶全成
【Go 夜读】#78 Go Schedular 源码阅读
总体讲一下调度器,再快速过一遍代码。
大纲
- 调度循环的建立
- GPM 状态机
- sysmon 线程做了什么?
分享者自我介绍
饶全成,Go 夜读 SIG 小组成员
分享时间
2020-03-11 21:00:00 UTC+8
Slides
https://qcrao.com/ishare/go-scheduler/
参考资料
- 【goroutine 调度器系列教程】http://mp.weixin.qq.com/mp/homepage?__biz=MzU1OTg5NDkzOA==&hid=1&sn=8fc2b63f53559bc0cee292ce629c4788&scene=18#wechat_redirect
- 【码农桃花源】https://qcrao.com/2019/09/02/dive-into-go-scheduler/
- 【欧神】https://github.com/changkun/go-under-the-hood
观看视频
{{< youtube id="B-ozWjqnX24" >}}
Night 79 2020 03 12 Go Micro Tools (`content/night/79-2020-03-12-go-micro-tools.md`)
desc: Go 夜读之 Go-Micro 运行时工具集 title: 第 79 期 Go-Micro 运行时工具集 date: 2020-03-12T21:00:00+08:00 author: 舒先
【Go 夜读】#79 Go-Micro 运行时工具集
介绍 Go-Micro 工具集的特性。
大纲
- Micro API(Gateway)
- Micro CLI(命令行工具)
- Micro Proxy(Go-Micro服务代理)
- 如果时间充裕:Micro 工具集的发展(Network,Run、Tunnel、Platform)
分享者自我介绍
Micro 中国团队
- 陈洪波:本期资料提供与审核,10年,成都云创新技术经理,Gopher、Phper,全栈码农,专注于微服务架构&服务网格技术的应用。
- Edward Chen:本期资料提供与审核,10年+,Honeywell HBT 研发技术专家,擅长Racher/K8S/Go/C#/C++/C/Typescript,专注于大规模IOT容器化微服务架构,一线开发。
- 舒先:本期资料提供与主讲,7年+金融级微服务开发经验,OPPO,Micro维护者团队成员,负责中国生态。
- 徐旭:本期资料提供与审核,应届,趣头条工程师,喜欢开源,Gev作者,擅长网络编程与表情包。
分享时间
2020-03-12 21:00:00 UTC+8
Slides
https://docs.google.com/presentation/d/1YVysPq1dByu87aGYdUVGhjyk_l6lDFw_AuHGDq3d6Bs/edit?usp=sharing
参考资料
- Go-Micro 框架与文档:https://micro.mu/docs/
- Micro 中国站文档:https://github.com/micro-in-cn/tutorials/
观看视频
{{< youtube id="_ZLB4BPZaks" >}}
Night 8 2018 05 31 Net Http Part2 (`content/night/8-2018-05-31-net-http-part2.md`)
title: 第 8 期 2018-05-31 线下活动 - Go 标准包阅读 date: 2018-05-31T11:49:10+08:00
参与人数: 10 人
Go 标准包阅读
Go 版本:go 1.10.2
net/http
- server.go
问题
func (s *Server) doKeepAlives() bool {
return atomic.LoadInt32(&s.disableKeepAlives) == 0 && !s.shuttingDown()
}为什么要用 atomic.LoadInt32(&s.disableKeepAlives) == 0 ?
原子操作比用锁更节约一点性能。
server.go#Shutdown 不保险
panicChan := make(chan interface{}, 1)
panicChan := make(chan interface{}, 1)
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
h.handler.ServeHTTP(tw, r)
close(done)
}()
select {
case p := <-panicChan:
panic(p)
...外部处理就不能按照你的意愿去处理了,如果不拿出来,那么进程就挂掉了。
// Deprecated: ErrWriteAfterFlush is no longer used.
ErrWriteAfterFlush = errors.New("unused")Header() Header 注释引发的Trailer的思考?
观看视频
{{< youtube id="U84dn76gixQ" >}}
延伸阅读
Night 80 2020 03 18 Go2 Generics (`content/night/80-2020-03-18-go2-generics.md`)
desc: Go 夜读之带你提前玩 Go 2 新特性:泛型 title: 第 80 期带你提前玩 Go 2 新特性:泛型 date: 2020-03-18T21:00:00+08:00 author: 欧长坤
【Go 夜读】#80 带你提前玩 Go 2 新特性:泛型
Go 2 的泛型到底是何方神圣?
泛型的内容不太好讲,一是因为对于熟悉泛型的人来说,Go 语言泛型的设计很好理解,几乎不需要介绍什么内容;二是因为目前 Go 语言泛型功能还不够丰富,虽然能够写出一部分泛型代码,但不够完美。
思前想后,这期分享的整体基调变成了讲述 Go 语言泛型设计的演变过程,为那些对泛型这一特性不够熟悉的人介绍 Go 语言泛型设计的变化,为什么成为了现在这个样子;并在介绍完具体设计后,通过实际编写几个例子来进一步理解目前泛型设计的优缺点。最后通过与 C++ 泛型设计的比较,来总结整个 Go 的泛型设计。
分享者自我介绍
欧长坤,Go 夜读 SIG 小组成员
分享时间
2020-03-18 21:00:00 UTC+8
Slides
https://changkun.de/s/go2generics/
观看视频
{{< youtube id="E16Y6bI2S08" >}}
Night 81 2020 03 19 Gorm Guide (`content/night/81-2020-03-19-gorm-guide.md`)
desc: Go 夜读之 gorm 介绍与展望 title: 第 81 期 gorm 介绍与展望 date: 2020-03-19T21:00:00+08:00 author: Jinzhu
【Go 夜读】#81 gorm 介绍与展望
对 gorm 的使用及设计做一些相关介绍,拓展。
分享者自我介绍
Jinzhu,gorm 作者
分享时间
2020-03-19 21:00:00 UTC+8
Slides
https://github.com/talkgo/night/files/4355218/GORM.pdf
观看视频
{{< youtube id="NCZHe6zb2Sg" >}}
Night 82 2020 03 21 Talkgo Night Story (`content/night/82-2020-03-21-talkgo-night-story.md`)
desc: Go 夜读之聊聊我们与 Go 夜读的故事以及效率效能学习分享 title: 第 82 期聊聊我们与 Go 夜读的故事以及效率效能学习分享 date: 2020-03-21T21:00:00+08:00 author: Go 夜读 SIG 小组,曹春晖,John,Darren
【Go 夜读】#82 聊聊我们与 Go 夜读的故事以及效率效能学习分享
分享者自我介绍
Go 夜读 SIG 小组,曹春晖,John,Darren
分享时间
2020-03-21 21:00:00 UTC+8
时间线
我们和 Go 夜读的故事:
00:00
印象最深的 Go 夜读分享:
04:36
欧神的日常:
17:46
你是怎么学 Go 的?
26:10
欧神 2018.03.21 学 Go 的第一天,也是 Go 夜读第 1 期的时间。
27:27
欧神谈学 Go 的方法,阅读 Go 官方文档。
28:35
推荐 Chrome 插件(Rooster, TimeYourWeb, picGo, Xnip)
36:24
alfred 等效率工具: 53:58
cch123: 1:06:58
star 项目管理?
watch 问题。。。
Go 后端的发展规划:
1:15:55
如何提高自己的认知/理解能力?有没有方法?听听大家怎么说。
1:22:30
谈谈安全
1:35:30
观看视频
{{< youtube id="gk510UOJoBA" >}}
Night 83 2020 03 26 Gobench (`content/night/83-2020-03-26-gobench.md`)
desc: Go 夜读之对 Go 程序进行可靠的性能测试 title: 第 83 期对 Go 程序进行可靠的性能测试 date: 2020-03-26T21:00:00+08:00 author: 欧长坤
【Go 夜读】#83 对 Go 程序进行可靠的性能测试
性能测试是 Go 语言工具链中比较重要的一环。我们已经知道了如何编写一个形如 func BenchmarkFunc(b *testing.B) 形式的的性能测试。那他的目标是什么?是否对其进行独立运行就已经足够了?如果不是,那如何才能正确的进行性能测试?又如何判断我们性能测试得到的结果是可靠的?
benchstat 作为 Go 语言工具链的一环,为我们提供了进行可靠性能测试的必要条件之一。那么 benchstat 又是什么?benchstat 能够为我们做哪些事情?它的基本原理又是什么?有了 benchstat 之后我们就可以「高枕无忧」了吗?我们还需要什么其他的工具吗?
本次分享我们将就上述这些问题展开,讨论如何在 Go 语言中进行可靠的性能测试。
大纲
- 准备可靠的测试环境
- benchstat
- 例子与实践
- 对代码块进行性能调优
- Benchmark 的正确性分析
- 其他的影响因素
- benchstat 中假设检验的原理
- 局限性及应对措施
分享者自我介绍
欧长坤,Go 夜读 SIG 小组成员
分享时间
2020-03-26 21:00:00 UTC+8
时间线
- 准备可靠的测试环境 00:00
- benchstat 09:25
- benchstat 原理 24:15
- runtime.memequal 的源码阅读 40:20
- 假设检验的原理 45:50
- 局限于应对措施 perflock 1:07:25
- perflock 原理 1:11:46
- 红黑树 1:24:40
- 手把手教你画出高大尚的图表📈1:28:30
- terminal 吐血分享 1:29:50
- Go map 的一个问题 1:33:50
- 数学的重要性 1:35:00
- Github 案例剖析答疑 1:35:50
Slides
https://changkun.de/s/gobench/
观看视频
{{< youtube id="RXM9cDzWZME" >}}
Night 84 2020 04 02 Go Aligned (`content/night/84-2020-04-02-go-aligned.md`)
desc: Go 夜读之图解 Go 内存对齐 title: 第 84 期图解 Go 内存对齐 date: 2020-04-02T21:00:00+08:00 author: 苗蕾@ThoutWorks
【Go 夜读】#84 图解 Go 内存对齐
关于内存对齐总会有各种声音?为什么要对齐,怎么对齐,不对齐有什么影响么?
这些声音可以离我们很远,也可以很近,比如:
当你想弄明白WaitGroup.state1为什么是[3]uint32而不是[12]byte
当你想知道struct是否占用内存是否合理
当你不希望在32位系统上原子操作int64|uint64时发生panic
当你闲着没事就是想读读源码提升下逼格。。。
本次分享借自己研究内存对齐的一些代码及源码示例,为大家带来Go里边的内存对齐是什么样的,以及如何利用内存对齐优化数据结构,提高代码的平台兼容性。
大纲
- 了解内存对齐的收益
- 为什么要对齐
- 怎么对齐:
- 数据结构对齐
- 内存地址对齐
- 64位字的安全访问保证(32位平台)
分享者自我介绍
苗蕾,Thoughtworks,搬砖工。
分享时间
2020-04-02 21:00:00 UTC+8
Slides
https://docs.google.com/presentation/d/1XUA8WfgTHCF_8XdfPEuNvs-WZ0DshFHKFEEqHRd3Tzg/edit?usp=sharing
观看视频
{{< youtube id="8a2G2MXRUxw" >}}
Night 85 2020 04 16 Douyu Confgo (`content/night/85-2020-04-16-douyu-confgo.md`)
desc: Go 夜读之斗鱼 Minerva 配置中心设计与实现 title: 第 85 期斗鱼 Minerva 配置中心设计与实现 date: 2020-04-16T21:00:00+08:00 author: 杜旻翔@斗鱼
【Go 夜读】#85 斗鱼 Minerva 配置中心设计与实现
介绍斗鱼 Minerva 配置中心的功能,以及后续的开源计划。
大纲
- 为什么需要配置中心
- 什么是 Confgo
- 为什么选择 Confgo
- Confgo 技术点
- Confgo 概览
分享者自我介绍
杜旻翔,武汉斗鱼科技有限公司,研发工程师,三年 Go 语言开发经验。
分享时间
2020-04-16 21:00:00 UTC+8
Slides
https://docs.google.com/presentation/d/1nNjE5KjAsi-vpDtwNhdMJkLG8Uvnk1SYLWXJnI8n8mk/edit?usp=sharing
观看视频
{{< youtube id="i-Q3x1PBqD0" >}}
Night 86 2020 04 23 Go Unsafe Pointer (`content/night/86-2020-04-23-go-unsafe-pointer.md`)
desc: Go 夜读之 Go 中非类型安全指针相关的事实和使用规则 title: 第 86 期 Go 中非类型安全指针相关的事实和使用规则 date: 2020-04-23T21:00:00+08:00 author: 杜旻翔@斗鱼
【Go 夜读】#86 Go 中非类型安全指针相关的事实和使用规则
Go 是一门支持自动垃圾回收和自动保护内存安全的语言。但是有时候 Go 运行时(runtime)提供的安全机制限制了某些细节和功能不能采用效率最高的方法来实现。非类型安全指针可以帮助我们绕开这些限制,但同时也打破了 Go 运行时处心积虑构建起来的安全屏障。在使用非类型安全指针时,我们必须谨慎地遵照 Go 官方文档中提出的建议,否则将产生一些很难觉察和很难定位的 bug。
本次分享将讲解和非类型安全指针相关的事实/规则,以及使用中的注意事项。
大纲
- 非类型安全指针相关的事实
- 非类型安全指针相关的类型转换规则
- 非类型安全指针相关的使用规则
分享者自我介绍
老貘,《Go 101》一书作者。
分享时间
2020-04-23 21:00 UTC+8
Slides
https://github.com/yaxinlx/go-nightreading-unsafe
观看视频
{{< youtube id="a_9oLmeFvwk" >}}
Night 87 2020 04 29 Goland Tips (`content/night/87-2020-04-29-goland-tips.md`)
desc: Go 夜读之 JetBrains GoLand 2020.1 新特性介绍 title: 第 87 期 JetBrains GoLand 2020.1 新特性介绍 date: 2020-04-29T21:00:00+08:00 author: Florin Pățan & 范圣佑
【Go 夜读】#87 JetBrains GoLand 2020.1 新特性介绍
由 JetBrains 打造的 GoLand 是专门为 Gopher 量身定制的全功能 IDE,若能善用其特性将可大大提升工作产能。随着 2020.1 版本的更新,带来了更多方便的新特性。距离上次分享已间隔将近一年,这次将专注为大家演示这些新引入的特性,希望将效率再往上提升一个层次。
大纲
- GoLand 基本使用介绍;
- Go 高效实战开发;
- 授权码释疑;
分享者自我介绍
Florin Pățan (以英语宣讲)
GoLand 技术布道师 @JetBrains
Florin 是资深 Gopher,参与过众多高性能、分散式的项目,目前在 JetBrains 担任 Goland 技术布道师。他在全世界有 Gopher 的地方出没,协助 Gopher 们能更高效的运用 Go 语言及 GoLand 做开发。
范圣佑 (协助翻译)
技术布道师 @JetBrains
圣佑是 JetBrains 技术布道师,负责推广 JetBrians 相关技术与产品,包括:Kotlin 编程语言、 IntelliJ IDEA 系列 IDE 及 YouTrack、TeamCity、Upsource 等团队合作解决方案,协助开发者善用工具辅助来提升生产力,同时维护代码品质。
分享时间
2020-04-29 21:00 UTC+8
参考资料
https://www.jetbrains.com/go/whatsnew
观看视频
{{< youtube id="QGEtbFtLMAc" >}}
Night 9 2018 06 14 Net Http Part3 (`content/night/9-2018-06-14-net-http-part3.md`)
title: 第 9 期 2018-06-14 线下活动 date: 2018-06-14T11:49:10+08:00
参与人数: 12 人
Go 标准包阅读
Go 版本:go 1.10.2
net/http
- server.go
- h2_bundle.go
问题
- WriteHeader(statusCode int)
- 要先调用 header.set()
- 再调用 WriteHeader()
- 然后调用 Write()
- 如果在调用 Write() 之后,还有比较多的逻辑要处理,则一定要紧跟着马上调一下 Flush()
- 然后调用 Flush()
HTTP2 不支持 Hijacker
使用了 Hijacker 之后不能再使用 Request.Body
type Hijacker interface {
// After a call to Hijack, the original Request.Body must not be used.
Hijack() (net.Conn, *bufio.ReadWriter, error)
}The returned bufio.Reader may contain unprocessed buffered data from the client.
CloseNotifier 主要用于 HTTP2
CloseNotify may wait to notify until Request.Body has been fully read.
HTTP2 中是如何在 net/http/server.go 中调用 serve() 触发的呢?
if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initNPNRequest{tlsConn, serverHandler{c.server}}
fn(c.server, tlsConn, h)
}
return
}主要是 TLSNextProto ,然后查询得到 onceSetNextProtoDefaults() 调用。
// onceSetNextProtoDefaults configures HTTP/2, if the user hasn't
// configured otherwise. (by setting srv.TLSNextProto non-nil)
// It must only be called via srv.nextProtoOnce (use srv.setupHTTP2_*).
func (srv *Server) onceSetNextProtoDefaults() {
if strings.Contains(os.Getenv("GODEBUG"), "http2server=0") {
return
}
// Enable HTTP/2 by default if the user hasn't otherwise
// configured their TLSNextProto map.
if srv.TLSNextProto == nil {
conf := &http2Server{
NewWriteScheduler: func() http2WriteScheduler { return http2NewPriorityWriteScheduler(nil) },
}
srv.nextProtoErr = http2ConfigureServer(srv, conf)
}
}然后是调用如下代码:
#h2_bundle.go
...
// ConfigureServer adds HTTP/2 support to a net/http Server.
//
// The configuration conf may be nil.
//
// ConfigureServer must be called before s begins serving.
func http2ConfigureServer(s *Server, conf *http2Server) error {
...
}- HTTP1 流水线,一条连接一个并发;HTTP2 是每个连接一个并发,每处理一个请求又是一个并发。
延伸阅读
- HTTP2 协议
- 多路复用
Other Awesome Tools Mac (`content/other/awesome-tools-mac.md`)
title: MAC下常用的开发工具集 date: 2018-12-25T00:00:00+08:00
MAC下常用的开发工具集
编辑器/IDE
-
推荐:⭐️⭐️⭐️⭐️⭐️
是一个轻量且强大的代码编辑器,支持Windows,OS X和Linux。内置JavaScript、TypeScript和Node.js支持,而且拥有丰富的插件生态系统,可通过安装插件来支持GO、C++、C#、Python、PHP等其他语言。
-
推荐:⭐️⭐️⭐️⭐️⭐️
Goland是由JetBrains公司旨在为go开发者提供的一个符合人体工程学的新的商业IDE。这个IDE整合了IntelliJ平台的有关go语言的编码辅助功能和工具集成特点。
-
推荐:⭐️⭐️⭐️⭐️
Sublime Text:一款具有代码高亮、语法提示、自动完成且反应快速的编辑器软件,不仅具有华丽的界面,还支持插件扩展机制,用她来写代码,绝对是一种享受。
-
推荐:⭐️⭐️⭐️⭐️⭐️
Vim是一个类似于Vi的著名的功能强大、高度可定制的文本编辑器,在Vi的基础上改进和增加了很多特性。
-
推荐:⭐️⭐️⭐️⭐️⭐️
Emacs,著名的集成开发环境和文本编辑器。Emacs被公认为是最受专业程序员喜爱的代码编辑器之一,另外一个是楼上的vim。
数据库工具推荐
- SequelPro(MySQL Client GUI)
- Robot 3T(MongoDB Client GUI)
- mycli(MySQL command line client)
- Navicat Premium(DB Client GUI)
- DataGrip: Database management systems by JetBrains
- SQLyog is an all-round Management Tool (/'GUI'/'Frontend') for MySQL
HTTP 压力测试工具
压缩\解压工具
-
推荐:⭐️⭐️⭐️⭐️⭐️
免费开源、除了LOGO比较丑,功能相对来说算是Mac下最好用的解压工具了
Other Dev Tools Vimrc (`content/other/dev-tools-vimrc.md`)
title: Go 开发工具 vim配置 date: 2018-12-06T00:00:00+08:00
粘贴代码块中的配置 $HOME/.vimrc 文件
/* Detailed source-code truncated for AI context efficiency. */Other Goland Help (`content/other/goland-help.md`)
title: Go 开发工具 date: 2018-12-06T00:00:00+08:00
GoLand
GoLand 怎么修改主题样式?
Preferences...-->Editor-->Color Scheme-->选择你喜欢的主题,然后点击 Apply,即可实时查看。
GoLand 下载新的主题
Plugins-->material Theme UI-->点击 Install,然后重启 GoLand 即可。
GoLand 函数 doc 能不能悬停提示?
Editor-->General-->Other -> Show quick documentation on mouse move Delay:500 ms,点击应用即可查看。
GoLand 保存时自动格式化
GoLand/Intellij Idea 可以帮助我们在保存时自动格式化代码.
Preferences...-->Tools-->File watchers-->go fmt/go imports,点击 OK 即可生效。
用了 GoLand 会将自己的配置文件覆盖,怎么办?
配置选项取消勾选就行:
Preferences...-->Tools-->Terminal-->将 shell integration 打勾取消,点击 OK 即可生效。
GoLand 怎么选中光标及其往后的所有内容?
VS Code 里面是通过 command+shift+向下箭头。
GoLand 怎么到文件首行?
VS Code 里面是通过 command+向上箭头。
Proposal Changes To Go (`content/proposal/changes-to-go.md`)
title: Proposing Changes to Go date: 2018-12-01T00:00:00+08:00
Introduction
The Go project's development process is design-driven.
Significant changes to the language, libraries, or tools must be first
discussed, and sometimes formally documented, before they can be implemented.
This document describes the process for proposing, documenting, and
implementing changes to the Go project.
To learn more about Go's origins and development process, see the talks
How Go Was Made,
The Evolution of Go,
and Go, Open Source, Community
from GopherCon 2015.
The Proposal Process
The proposal process is the process for reviewing a proposal and reaching
a decision about whether to accept or decline the proposal.
The proposal author creates a brief issue describing the proposal.
Note: There is no need for a design document at this point.
Note: A non-proposal issue can be turned into a proposal by simply adding the proposal label.A discussion on the issue tracker aims to triage the proposal into one of three outcomes:
- Accept proposal, or
- decline proposal, or
- ask for a design doc.
If the proposal is accepted or declined, the process is done.
Otherwise the discussion is expected to identify concerns that
should be addressed in a more detailed design.The proposal author writes a design doc to work out details of the proposed
design and address the concerns raised in the initial discussion.Once comments and revisions on the design doc wind down, there is a final
discussion on the issue, to reach one of two outcomes:- Accept proposal or
- decline proposal.
After the proposal is accepted or declined (whether after step 2 or step 4),
implementation work proceeds in the same way as any other contribution.
Detail
Goals
- Make sure that proposals get a proper, fair, timely, recorded evaluation with
a clear answer. - Make past proposals easy to find, to avoid duplicated effort.
- If a design doc is needed, make sure contributors know how to write a good one.
Definitions
- A proposal is a suggestion filed as a GitHub issue, identified by having
the Proposal label. - A design doc is the expanded form of a proposal, written when the
proposal needs more careful explanation and consideration.
Scope
The proposal process should be used for any notable change or addition to the
language, libraries and tools.
Since proposals begin (and will often end) with the filing of an issue, even
small changes can go through the proposal process if appropriate.
Deciding what is appropriate is matter of judgment we will refine through
experience.
If in doubt, file a proposal.
Compatibility
Programs written for Go version 1.x must continue to compile and work with
future versions of Go 1.
The Go 1 compatibility document describes
the promise we have made to Go users for the future of Go 1.x.
Any proposed change must not break this promise.
Language changes
Go is a mature language and, as such, significant language changes are unlikely
to be accepted.
A "language change" in this context means a change to the
Go language specification.
(See the release notes for
examples of recent language changes.)
Design Documents
As noted above, some (but not all) proposals need to be elaborated in a design document.
The design doc should be checked in to the proposal repository as
design/NNNN-shortname.md,
whereNNNNis the GitHub issue number andshortnameis a short name
(a few dash-separated words at most).
Clone this repository withgit clone https://go.googlesource.com/proposal
and follow the usual Gerrit workflow for Go.The design doc should follow the template.
The design doc should address any specific concerns raised during the initial discussion.
It is expected that the design doc may go through multiple checked-in revisions.
New design doc authors may be paired with a design doc "shepherd" to help work on the doc.For ease of review with Gerrit, design documents should be wrapped around the
80 column mark.
Each sentence should start on a new line
so that comments can be made accurately and the diff kept shorter.- In Emacs, loading
fill.elfrom this directory will makefill-paragraphformat text this way.
- In Emacs, loading
Comments on Gerrit CLs should be restricted to grammar, spelling,
or procedural errors related to the preparation of the proposal itself.
All other comments should be addressed to the related GitHub issue.
Quick Start for Experienced Committers
Experienced committers who are certain that a design doc will be
required for a particular proposal
can skip steps 1 and 2 and include the design doc with the initial issue.
In the worst case, skipping these steps only leads to an unnecessary design doc.
Proposal Review
A group of Go team members holds “proposal review meetings”
approximately weekly to review pending proposals.
The principal goal of the review meeting is to make sure that proposals
are receiving attention from the right people,
by cc'ing relevant developers, raising important questions,
pinging lapsed discussions, and generally trying to guide discussion
toward agreement about the outcome.
The discussion itself is expected to happen on the issue tracker,
so that anyone can take part.
The proposal review meetings also identify issues where
consensus has been reached and the process can be
advanced to the next step (by marking the proposal accepted
or declined or by asking for a design doc).
Consensus and Disagreement
The goal of the proposal process is to reach general consensus about the outcome
in a timely manner.
If general consensus cannot be reached,
the proposal review group decides the next step
by reviewing and discussing the issue and
reaching a consensus among themselves.
If even consensus among the proposal review group
cannot be reached (which would be exceedingly unusual),
the arbiter (rsc@)
reviews the discussion and
decides the next step.
Help
If you need help with this process, please contact the Go contributors by posting
to the golang-dev mailing list.
(Note that the list is moderated, and that first-time posters should expect a
delay while their message is held for moderation.)
To learn about contributing to Go in general, see the
contribution guidelines.
Articles How To Profile How To Profile (`content/articles/how_to_profile/how_to_profile.md`)
title: "HOW TO PROFILE" date: 2019-01-24T15:09:31+08:00 draft: true
介绍使用Benchmark进行调优。
- 如何写压测示例;
- 如何使用压测程序进行调优;
原文及源码参考:
- how to profile
- 交流加微信:
imzouying
原始代码
代码功能:访客记次数。
具体程序参见上一篇how to test
package main
import (
"fmt"
"net/http"
"sync"
"github.com/sirupsen/logrus"
)
var counter = map[string]int{}
var mu sync.Mutex // mutex for counter
func handleHello(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
mu.Lock()
defer mu.Unlock()
counter[name]++
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
"'>Welcome!</h1> Name: " + nam