Are you over 18 and want to see adult content?
More Annotations
![تجارب - منصّة عربية لنشر وتبادل التجارب والخبرات الجامعية](https://www.archivebay.com/archive/fc7ba1fb-4e85-47e3-a546-4dcebb4bde86.png)
تجارب - منصّة عربية لنشر وتبادل التجارب والخبرات الجامعية
Are you over 18 and want to see adult content?
![ReqDirect Plus | There's a new way to look at it.](https://www.archivebay.com/archive/0f37fdb0-d934-41e1-887f-2068cda68041.png)
ReqDirect Plus | There's a new way to look at it.
Are you over 18 and want to see adult content?
![Πρώτη Καθημερινή Εφημερίδα της Ηλείας, νέα από την Ηλεία](https://www.archivebay.com/archive/e41c2eb2-2ad3-4ab5-a9f8-51cf0e781ee2.png)
Πρώτη Καθημερινή Εφημερίδα της Ηλείας, νέα από την Ηλεία
Are you over 18 and want to see adult content?
![Romanian Journal of Morphology and Embryology | Rom J Morphol Embryol | RJME](https://www.archivebay.com/archive/aa476951-3766-4bc7-90f8-08eea4e06420.png)
Romanian Journal of Morphology and Embryology | Rom J Morphol Embryol | RJME
Are you over 18 and want to see adult content?
![Leggari - Your source for epoxy countertops, flooring, and table kits - Leggari](https://www.archivebay.com/archive/d3d320ff-3550-4f48-87b8-7b25c5fe5e5d.png)
Leggari - Your source for epoxy countertops, flooring, and table kits - Leggari
Are you over 18 and want to see adult content?
![Home - Ferienwelt Kristall Rauris](https://www.archivebay.com/archive/f3d72442-f873-4af1-b214-c84a49152cbe.png)
Home - Ferienwelt Kristall Rauris
Are you over 18 and want to see adult content?
Favourite Annotations
![A complete backup of studenttutors.com.au](https://www.archivebay.com/archive/679f207b-097d-4c62-967d-2400251d3828.png)
A complete backup of studenttutors.com.au
Are you over 18 and want to see adult content?
![A complete backup of flinders.vic.edu.au](https://www.archivebay.com/archive/38197c76-b0f2-4602-8bf1-0dbdeaec5402.png)
A complete backup of flinders.vic.edu.au
Are you over 18 and want to see adult content?
![A complete backup of turkmenelitv.com](https://www.archivebay.com/archive/525336be-5399-491f-996b-83fc84bb30f2.png)
A complete backup of turkmenelitv.com
Are you over 18 and want to see adult content?
![A complete backup of mambo-support.org](https://www.archivebay.com/archive/1ffba27b-7f00-43e5-8d83-28dacc85a4b3.png)
A complete backup of mambo-support.org
Are you over 18 and want to see adult content?
Text
TONY BAI
Tony Bai
WWW.TONYBAI.COM
www.tonybai.com
一个有关GOLANG变量作用域的坑 一个有关Golang变量作用域的坑. 临近下班前编写和调试一段 Golang 代码,但运行结果始终与期望不符,怪异的很,下班前依旧无果。. 代码Demo如下:. 这段代码原意是定义一个包内全局变量p,用foo ()的返回值对p进行初始化,在bar中使用p。. 预期结果:bar ()和main TONY BAITRANSLATE THIS PAGE “Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足 解决登录HARBOR REGISTRY时鉴权失败的问题 2、查看两个Harbor node上的registry log,弄清问题现象. 将/etc/hosts中hub.my-domain.com的硬解析删除,恢复DNS解析。. 打开两个terminal tab分别监视连个Harbor node上的registry的日志。. 经过几次测试,发现一个现象:当docker login成功时,都是一个node上的日志出现更 也谈GO的可移植性 Go有很多优点,比如:简单、原生支持并发等,而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗?本着“知其然,亦要知其所以然”的精神,本文我们就来探究一下GoTONY BAI
Tony Bai
WWW.TONYBAI.COM
www.tonybai.com
一个有关GOLANG变量作用域的坑 一个有关Golang变量作用域的坑. 临近下班前编写和调试一段 Golang 代码,但运行结果始终与期望不符,怪异的很,下班前依旧无果。. 代码Demo如下:. 这段代码原意是定义一个包内全局变量p,用foo ()的返回值对p进行初始化,在bar中使用p。. 预期结果:bar ()和main TONY BAITRANSLATE THIS PAGE “Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足WWW.TONYBAI.COM
www.tonybai.com
TONY BAI
Tony Bai
一个有关GOLANG变量作用域的坑 一个有关Golang变量作用域的坑. 临近下班前编写和调试一段 Golang 代码,但运行结果始终与期望不符,怪异的很,下班前依旧无果。. 代码Demo如下:. 这段代码原意是定义一个包内全局变量p,用foo ()的返回值对p进行初始化,在bar中使用p。. 预期结果:bar ()和main 使用FUNCTRACE辅助进行GO项目源码分析 “Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足Tony Bai
一个程序员的心路历程* 关于我
* 文章列表
官宣:GO专栏“改善GO语言编程质量的50个有效实践”上线了* 九月 8, 2020
* 4 条评论
断断续续写了一年多的Go专栏:《改善Go语言编程质量的50个有效实践》 今天终于正式上线了!- https://www.imooc.com/read/87 慕课专栏:《改善Go语言编程质量的50个有效实践》 Go语言 是Google大牛团队(Robert Griesemer、Rob Pike以及Ken Thompson)设计的一种静态类型、编译型编程语言,支持垃圾回收和轻量级并发,它于2009年11月诞生,一面世就以语法简单、原生支持并发、标准库强大、工具链丰富等优点吸引了大量开发者。经过10余年演化和发展 ,Go如今已成为云基础架构的标准编程语言 ,很多云原生时代的杀手级平台、中间件、协议和应用都是采用Go语言开发的,比如:Docker、Kubernetes
、以太坊
、Hyperledger Fabric超级账本 、新一代互联网基础设施协议ipfs等。
Go是一门特别容易入门的编程语言,无论是刚出校门的新手还是从其他编程语言转过来的成手,都可以在短时间内快速掌握Go语法并投入到Go代码的编写中。但笔者在日常收到很多Go初学者的疑问:GO入门容易,但进阶难,怎么才能像GO团队那样写出符合GO思维和语言惯例(IDIOMATIC)的高质量代码呢? 这个问题也引发了我的思考。在2017年GopherChina大会 上笔者以演讲的形式初次尝试回答这个问题 ,但鉴于演讲的时长有限,很多内容难于展开,效果不甚理想。而这个慕课网专栏则是我对解答这个问题作出的第二次尝试。 这次解答的思路有两个:*
思维层面:写出高质量Go代码的前提是思维方式的进阶,即使用GO语言的思维去写GO代码;*
实践技巧层面:Go标准库、优秀Go开源库是一个挖倔高质量、符合Go惯用法的Go代码的宝库,对其进行阅读、挖掘和整理归纳,我们可以得到一些帮助我们快速进阶的有效实践。 本专栏正是基于上面思路为想实现GO进阶但又不知从何入手的你而设的。 首届图灵奖得主、著名计算机科学家艾伦·佩利(Alan J. Perlis)曾经说过:“不能影响到你的编程思维方式的编程语言不值得去学习和使用”,足见编程思维对编程语言学习和应用的重要性。只有真正领悟了一门编程语言的设计哲学和编程思维,并将其应用到日常编程当中去,你才算是真正地实现了在这门编程语言上的进阶。 因此,本专栏首先将带领大家回顾Go语言的演化历史,一起了解并深刻体会Go大牛们在设计Go语言时的所思所想,与大牛们实现思维上的共鸣,理清那些看似随意的,实则经过深思熟虑的设计的背后的付出。 接下来,本专栏将基于笔者对Go核心团队、Go社区高质量代码的分析归纳,从代码风格、基础语法、函数/方法、接口、并发、错误处理、测试调试、标准库、工程实践等多个方面给出改善Go代码质量,写出符合Go思维和惯例的代码的有效实践。 学习了本专栏的这50条有效实践,你将拥有和GO大牛们一样GO编程思维,写出符合GO惯例风格的高质量GO代码,从众多GO入门选手中脱颖而出,快速实现从GO编程新手到专家的转变! 本专栏共分10个模块(篇),50个小节。 * 模块1:设计哲学篇 本专栏的开篇和总起。和读者一起穿越时空,回顾历史,详细了解Go语言的诞生、演化以及今天的发展。归纳总结Go语言的设计哲学和原生编程思维,让读者可以站在语言设计者的高度理解Go语言与众不同的设计,在更高层次,形成共鸣,产生认同。只有强烈认同,才能更上一层楼。 * 模块2:代码风格篇 每种编程语言都有自己惯用的代码风格,而遵循语言惯用风格是高质量Go代码的必要条件。本篇详细介绍了得到公认且广泛使用的Go工程的结构布局、代码风格标准、标识符命名惯例以及变量声明形式等。 * 模块3:基础语法篇 本模块详述在基础语法层面高质量Go代码的惯用法和有效实践,涵盖无类型常量的作用、定义Go的“枚举常量”、“零值可用”类型的意义、切片原理以及其高效的原因、Go包导入路径的真正含义等。 * 模块4:函数与方法篇 函数和方法是Go程序的基本组成单元。本模块聚焦于函数与方法的设计与实现,涵盖init函数的使用、跻身“一等公民”行列的函数有何不同、Go方法的本质等帮助读者深入理解它们的内容。 * 模块5:接口篇 接口是Go语言中的“魔法师”。本模块将聚焦接口,涵盖接口的设计惯例、使用接口类型的注意事项以及接口类型对代码可测试性的影响等。 * 模块6:并发编程篇 Go以其轻量级的并发模型而闻名。本模块将详细介绍Go基本执行单元–
goroutine的调度原理、Go并发模型以及常见并发模式、Go支持并发的原生类型-channel的惯用使用模式等内容。 * 模块7:错误处理篇 Go语言十分重视错误处理,它有着相对保守的设计和显式处理错误的惯例。本模块将涵盖Go错误处理的哲学以及在这套哲学下一些常见错误处理问题的优秀实践方案。 * 模块8:测试与调试篇 Go自带强大且为人所称道的工具链,本模块将详细介绍Go在单元测试、性能测试以及代码调试方面的最佳实践方案。 * 模块9:标准库篇 Go拥有功能强大且质量上乘的标准库,多数情况我们仅使用标准库所提供的功能而不借助第三方库就可实现应用的大部分功能,这大幅降低学习成本以及代码依赖的管理成本。本模块将详细说明高频使用的标准库包,如net/http、strings、bytes、time等的正确使用方式,以及reflect包、cgo在使用时的注意事项。 * 模块10:工程实践篇 本模块将涵盖我们使用Go语言做软件项目过程中很大可能会遇到的一些工程问题的解决方法,包括:使用module进行Go包依赖管理、Go应用容器镜像、Go相关工具使用以及Go语言的避“坑”指南。 从上述专栏结构,我们也能看出本专栏并不是Go入门的最佳选择。如果非要给本专栏划定一个目标人群,或者说哪些读者阅读本专栏后会更多受益,我觉得是那些已经迈入GO语言世界、但迫切希望进一步提升层次、写出高质量GO代码的GO开发者。 很多朋友可能会问?你这个专栏有何与众不同之处?在专栏上线前编辑老师也让我编写课程亮点,我觉得下面这几句话可以概括专栏的特点:* 进阶必备 –
50个有效实践助你掌握高效Go程序设计之道; * 高屋建瓴 – Go设计哲学与编程思想先行; * 深入浅出 – 原理深入,例子简明,讲解透彻;* 图文并茂 –
大量图表辅助学习,重点难点轻松掌控; * 覆盖全面 – 覆盖高级面试知识点,求职更自信。 本专栏第一次落笔大约在Go 1.12 发布后,大约将在今年10月份,即在Go1.15
发布后的第二个月完成。这中间有一定的跨度,因此专栏内的有些内容在各个Go版本间可能会有差异。笔者在内容中已经尽量做了版本适用标识,但难免有疏漏。各位读者在遇到问题时,可以及时反馈给我。 此外,Go语言还在飞速发展,一些当前的惯用表达方式或有效实践可能在日后因语言引入新的特性(比如:Go泛型 )而“过时”。我会在我的博客上持续关注Go语言的演化,并将最新的Go高效编程实践分享给大家。 最后再来一次自我介绍:TONY BAI,Go语言技术专家和鼓吹者,GopherChina大会讲师,Go语言技术博客tonybai.com 的作者,GopherDaily(Go日报)项目 (github.com/bigwhite/gopherdaily)维护者,OSCHINA源创会技术讲师 ,《七周七语言》 译者之一,慕课网《Kubernetes实战:高可用集群搭建、配置、运维与应用》 作者,开源拥趸。
作为一名在国内接触Go语言较早(2012年)的Gopher和Go布道师,Tony Bai拥有丰富的Go开发知识和经验。他在个人博客上撰写了大量关于Go语言的文章,并深受Go社区欢迎。目前他正在国内一大型软件公司带领团队使用Go语言构建移动运营商的5G消息平台,这个平台将处理来自全国各地几十万个5G chatbot程序每天发送的几十亿条5G消息请求。 欢迎大家订阅我的专栏! 如有意见和建议,可在我本博文后面的评论中反馈。感谢大家支持。 专栏涉及的源码仓库地址 :https://github.com/bigwhite/publication/tree/master/column/imooc/go-50tips/sources ------------------------- 我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践 ”上线了,欢迎大家订阅学习! 我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用 ”在慕课网上线了,感谢小伙伴们学习支持!我爱发短信
:企业级短信平台定制开发专家 https://51smspush.com/smspush :
可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。 2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5GRCS消息。
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1core
CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址 :https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 我的联系方式: 微博:https://weibo.com/bigwhite20xx 微信公众号:iamtonybai 博客:tonybai.com github: https://github.com/bigwhite微信赞赏:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。 使用FUNCTRACE辅助进行GO项目源码分析* 六月 4, 2021
* 0 条评论
本文永久链接
–
https://tonybai.com/2021/06/04/go-source-analysis-with-functrace 在《像跟踪分布式服务调用那样跟踪Go函数调用链》 一文中,我们介绍了一种跟踪函数调用链的思路,并给出了一种实现functrace :https://github.com/bigwhite/functrace。这个小工具不仅仅是分享给大家的,我自己在工作和学习时也在使用。最近发现这个小工具在阅读和分析某个Go项目源码时也能起到关键的辅助作用。这里就和大家简单讲解一下如何用functrace来辅助Go源码阅读和分析。 程序员的日常离不开“源码阅读和分析”,日常阅读代码的姿势无非是这么几种(或几种的组合):*
结合源码编辑器或IDE提供的强大的源码交叉索引和跳转功能在一个庞大的源码库中建立起代码间的联系;*
将代码跑起来,在代码中加上一些print输出,跟踪执行流并画出;*
也有人喜欢用调试器从一点(通常是main)开始单步跟踪执行流。 无论哪一种方式,最终只要时间够长,态度到位,总是会将代码分析出个七七八八的。 就笔者来看,无论是哪种范式:命令式、面向对象、函数式,最终梳理出来的源码脉络都是建立在执行基本单元(函数或方法)上,代码的执行主线(并发程序会有若干条)本质上就是一条函数/方法调用链。只要把这条链理出来,代码理解起来就不难了。上述的代码阅读方法实质也是参照这个逻辑的。只是对于调用层次较深,还伴随有回调的代码,梳理调用链难度高、效率低。 functrace最初用于跟踪函数调用链(得益于Go核心开发团队公开的抽象语法树ASTAPI
),但如果在阅读代码时直接用functrace输出函数调用链,那将大幅提高我们源码阅读分析的效率。下面我们就用一个样例项目来试试如何用functrace梳理出代码的执行主线。 我们以Go高性能、轻量级、非阻塞的事件驱动网络框架gnet为例,来看看如何阅读分析gnet的源码。首先我们需要安装functrace工具: $go install github.com/bigwhite/functrace/cmd/gen@latest go: downloading github.com/bigwhite/functrace v0.0.0-20210603024853-ccab68a2604c go: downloading golang.org/x/tools v0.0.0-20201204062850-545788942d5f$gen -h
gen xxx.go
-w write result to (source) file instead of stdout 接下来,我们下载要进行源码分析的gnet源码: $git clone git@github.com:panjf2000/gnet.git 我们进入gnet目录,现在我们可以使用gen命令为任意go源文件添加“跟踪设施”了,比如:$gen -w gnet.go
add trace for gnet.go ok $ git diff gnet.go diff --git a/gnet.go b/gnet.go index b4c04a5..a7afe2b 100644--- a/gnet.go
+++ b/gnet.go
@@ -29,6 +29,7 @@ import ("sync"
"time"
+ "github.com/bigwhite/functrace" "github.com/panjf2000/gnet/errors" "github.com/panjf2000/gnet/internal" "github.com/panjf2000/gnet/internal/logging"... ...
我们可以这样根据自己的需要在特定的go源文件上添加“跟踪设施”,但是多数情况下,我们也可以通过脚本为项目内所有go源文件批量添加“跟踪设施”,functrace项目提供了一个简单的脚本batch_add_trace.sh ,下面我们就来通过该脚本将gnet下的go源文件批量加上函数跟踪设施: 下载functrace源码: $git clone https://github.com/bigwhite/functrace.git 将functrace/scripts/batch_add_trace.sh 拷贝到上面gnet目录下并执行下面命令: # bash batch_add_trace.sh... ...
add trace for ./server_unix.go ok add trace for ./internal/socket/sockopts_posix.go ok... ...
add trace for ./ringbuffer/ring_buffer_test.go ok add trace for ./ringbuffer/ring_buffer.go ok no trace added for ./pool/bytebuffer/bytebuffer.go add trace for ./pool/goroutine/goroutine.go ok add trace for ./pool/ringbuffer/ringbuffer.go ok add trace for ./loop_linux.go ok add trace for ./server_windows.go ok 接下来我们编写一个基于gnet的程序,我们就使用gnet参加TechEmpower的那份代码:
//main.go
package main
import (
"bytes"
"flag"
"fmt"
"log"
"runtime"
"time"
"github.com/panjf2000/gnet")
type httpServer struct {*gnet.EventServer
}
type httpCodec struct {delimiter byte
}
func (hc *httpCodec) Encode(c gnet.Conn, buf byte) (out byte, err error) {return buf, nil
}
func (hc *httpCodec) Decode(c gnet.Conn) (out byte, err error) {buf := c.Read()
if buf == nil {
return
}
c.ResetBuffer()
// process the pipelinevar i int
pipeline:
if i = bytes.Index(buf, hc.delimiter); i != -1 { out = append(out, "HTTP/1.1 200 OK\r\nServer: gnet\r\nContent-Type: text/plain\r\nDate: "...) out = time.Now().AppendFormat(out, "Mon, 02 Jan 2006 15:04:05 GMT") out = append(out, "\r\nContent-Length: 13\r\n\r\nHello, World!"...)buf = buf
goto pipeline
}
// request not ready, yetreturn
}
func (hs *httpServer) OnInitComplete(srv gnet.Server) (action gnet.Action) { log.Printf("HTTP server is listening on %s (multi-cores: %t, loops: %d)\n", srv.Addr.String(), srv.Multicore, srv.NumEventLoop)return
}
func (hs *httpServer) React(frame byte, c gnet.Conn) (out byte, action gnet.Action) { // handle the requestout = frame
return
}
func init() {
runtime.GOMAXPROCS(runtime.NumCPU() * 2)}
func main() {
var port int
var multicore bool // Example command: go run main.go --port 8080 --multicore=true flag.IntVar(&port, "port", 8080, "server port") flag.BoolVar(&multicore, "multicore", true, "multicore")flag.Parse()
http := new(httpServer) hc := &httpCodec{delimiter: byte("\r\n\r\n")}// Start serving!
log.Fatal(gnet.Serve(http, fmt.Sprintf("tcp://:%d", port), gnet.WithMulticore(multicore), gnet.WithCodec(hc)))}
构建这份代码: $go mod init gnet-demo $go get github.com/panjf2000/gnet go: downloading github.com/panjf2000/gnet v1.4.5 go get: added github.com/panjf2000/gnet v1.4.5 //修改go.mod,使用replace让gnet-demo使用本地的gnet代码$cat go.mod
module gnet-demo
go 1.16
replace github.com/panjf2000/gnet => /root/go/src/github.com/panjf2000/gnetrequire (
github.com/panjf2000/gnet v1.4.5)
$go get github.com/bigwhite/functrace go get: added github.com/bigwhite/functrace v0.0.0-20210603024853-ccab68a2604c $go build -tags trace //-tags trace务必不能省略,这个是开启functrace的关键 构建后,我们来执行构建出的可执行程序:gnet-demo: $ go build -tags trace root@VM-0-12-ubuntu:~/test/go/gnet-demo# ./gnet-demo g: ->github.com/panjf2000/gnet/internal/socket.maxListenerBacklog g: <-github.com/panjf2000/gnet/internal/socket.maxListenerBacklog g: ->github.com/panjf2000/gnet/ringbuffer.New g: <-github.com/panjf2000/gnet/ringbuffer.New g: ->github.com/panjf2000/gnet/internal/logging.init.0 g: <-github.com/panjf2000/gnet/internal/logging.init.0 g: ->github.com/panjf2000/gnet.WithMulticore g: <-github.com/panjf2000/gnet.WithMulticore g: ->github.com/panjf2000/gnet.WithCodec g: <-github.com/panjf2000/gnet.WithCodec g: ->github.com/panjf2000/gnet.Serve g: ->github.com/panjf2000/gnet.loadOptions g: <-github.com/panjf2000/gnet.loadOptions g: ->github.com/panjf2000/gnet.parseProtoAddr g: <-github.com/panjf2000/gnet.parseProtoAddr g: ->github.com/panjf2000/gnet.initListener g: ->github.com/panjf2000/gnet.(*listener).normalize g: ->github.com/panjf2000/gnet/internal/socket.TCPSocket g: ->github.com/panjf2000/gnet/internal/socket.tcpSocket g: ->github.com/panjf2000/gnet/internal/socket.getTCPSockaddr g: ->github.com/panjf2000/gnet/internal/socket.determineTCPProto g: <-github.com/panjf2000/gnet/internal/socket.determineTCPProto g: <-github.com/panjf2000/gnet/internal/socket.getTCPSockaddr g: ->github.com/panjf2000/gnet/internal/socket.sysSocket g: <-github.com/panjf2000/gnet/internal/socket.sysSocket g: ->github.com/panjf2000/gnet/internal/socket.SetNoDelay g: <-github.com/panjf2000/gnet/internal/socket.SetNoDelay g: <-github.com/panjf2000/gnet/internal/socket.tcpSocket g: <-github.com/panjf2000/gnet/internal/socket.TCPSocket g: <-github.com/panjf2000/gnet.(*listener).normalize g: <-github.com/panjf2000/gnet.initListener g: ->github.com/panjf2000/gnet.serve 2021/06/03 14:53:30 HTTP server is listening on :8080 (multi-cores: true, loops: 1) g: ->github.com/panjf2000/gnet.(*server).start g: ->github.com/panjf2000/gnet.(*server).activateReactors g: ->github.com/panjf2000/gnet/internal/netpoll.OpenPoller g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: ->github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g: <-github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g: <-github.com/panjf2000/gnet/internal/netpoll.OpenPoller g: ->github.com/panjf2000/gnet.(*roundRobinLoadBalancer).register g: <-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).register g: ->github.com/panjf2000/gnet.(*server).startSubReactors g: ->github.com/panjf2000/gnet.(*roundRobinLoadBalancer).iterate g: <-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).iterate g: <-github.com/panjf2000/gnet.(*server).startSubReactors g: ->github.com/panjf2000/gnet/internal/netpoll.OpenPoller g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: ->github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g: <-github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g: <-github.com/panjf2000/gnet/internal/netpoll.OpenPoller g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: <-github.com/panjf2000/gnet.(*server).activateReactors g: <-github.com/panjf2000/gnet.(*server).start g: ->github.com/panjf2000/gnet.(*server).stop g: ->github.com/panjf2000/gnet.(*server).waitForShutdown g: ->github.com/panjf2000/gnet.(*server).activateMainReactor g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).Polling g: ->github.com/panjf2000/gnet/internal/netpoll.newEventList g: <-github.com/panjf2000/gnet/internal/netpoll.newEventList g: ->github.com/panjf2000/gnet.(*server).activateSubReactor g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).Polling g: ->github.com/panjf2000/gnet/internal/netpoll.newEventList g: <-github.com/panjf2000/gnet/internal/netpoll.newEventList 我们看到gnet的执行主线被清晰的打印出来,通过输出的函数所在包我们可以轻松找到对应的源文件。g这goroutine显然是main goroutine,整个程序的初始化线索通过跟踪g的函数链便一目了然。 如果我们要看gnet是如何处理一个外部链接的,我们可以向gnet-demo建立一个连接,看看gnet-demo的输出。 我们通过curl命令向gnet-demo发起一个http请求: $curl localhost:8080Hello, World!
gnet-demo输出:
g: ->github.com/panjf2000/gnet.(*server).acceptNewConnection g: ->github.com/panjf2000/gnet/internal/socket.SockaddrToTCPOrUnixAddr g: ->github.com/panjf2000/gnet/internal/socket.sockaddrInet6ToIPAndZone g: ->github.com/panjf2000/gnet/internal/socket.ip6ZoneToString g: <-github.com/panjf2000/gnet/internal/socket.ip6ZoneToString g: <-github.com/panjf2000/gnet/internal/socket.sockaddrInet6ToIPAndZone g: <-github.com/panjf2000/gnet/internal/socket.SockaddrToTCPOrUnixAddr g: ->github.com/panjf2000/gnet.(*roundRobinLoadBalancer).next g: <-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).next g: ->github.com/panjf2000/gnet.newTCPConn g: ->github.com/panjf2000/gnet/pool/ringbuffer.Get g: ->github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g: ->github.com/panjf2000/gnet/ringbuffer.New g: <-github.com/panjf2000/gnet/ringbuffer.New g: <-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g: <-github.com/panjf2000/gnet/pool/ringbuffer.Get g: ->github.com/panjf2000/gnet/pool/ringbuffer.Get g: ->github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g: ->github.com/panjf2000/gnet/ringbuffer.New g: <-github.com/panjf2000/gnet/ringbuffer.New g: <-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g: <-github.com/panjf2000/gnet/pool/ringbuffer.Get g: <-github.com/panjf2000/gnet.newTCPConn g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).Trigger g: ->github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Enqueue g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.cas g: <-github.com/panjf2000/gnet/internal/netpoll/queue.cas g: ->github.com/panjf2000/gnet/internal/netpoll/queue.cas g: <-github.com/panjf2000/gnet/internal/netpoll/queue.cas g: <-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Enqueue g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).Trigger g: <-github.com/panjf2000/gnet.(*server).acceptNewConnection g: ->github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: <-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: ->github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.cas g: <-github.com/panjf2000/gnet/internal/netpoll/queue.cas g: <-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g: ->github.com/panjf2000/gnet.(*eventloop).loopOpen g: ->github.com/panjf2000/gnet.(*eventloop).addConn g: <-github.com/panjf2000/gnet.(*eventloop).addConn g: ->github.com/panjf2000/gnet.(*EventServer).OnOpened g: <-github.com/panjf2000/gnet.(*EventServer).OnOpened g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: ->github.com/panjf2000/gnet.(*eventloop).handleAction g: <-github.com/panjf2000/gnet.(*eventloop).handleAction g: <-github.com/panjf2000/gnet.(*eventloop).loopOpen g: ->github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: ->github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.load g: <-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g: ->github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Empty g: <-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Empty g: ->github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: <-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: ->github.com/panjf2000/gnet.(*eventloop).loopRead g: ->github.com/panjf2000/gnet.(*conn).read g: ->github.com/panjf2000/gnet.(*conn).Read g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet.(*conn).Read g: ->github.com/panjf2000/gnet.(*conn).ResetBuffer g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet.(*conn).ResetBuffer g: <-github.com/panjf2000/gnet.(*conn).read g: ->github.com/panjf2000/gnet.(*EventServer).PreWrite g: <-github.com/panjf2000/gnet.(*EventServer).PreWrite g: ->github.com/panjf2000/gnet.(*conn).write g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet.(*conn).write g: ->github.com/panjf2000/gnet.(*conn).read g: ->github.com/panjf2000/gnet.(*conn).Read g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet.(*conn).Read g: ->github.com/panjf2000/gnet.(*conn).ResetBuffer g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet.(*conn).ResetBuffer g: <-github.com/panjf2000/gnet.(*conn).read g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Write g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Write g: <-github.com/panjf2000/gnet.(*eventloop).loopRead g: ->github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: <-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: ->github.com/panjf2000/gnet.(*eventloop).loopRead g: ->github.com/panjf2000/gnet.(*eventloop).loopCloseConn g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g: ->github.com/panjf2000/gnet/internal/netpoll.(*Poller).Delete g: <-github.com/panjf2000/gnet/internal/netpoll.(*Poller).Delete g: ->github.com/panjf2000/gnet.(*eventloop).addConn g: <-github.com/panjf2000/gnet.(*eventloop).addConn g: ->github.com/panjf2000/gnet.(*EventServer).OnClosed g: <-github.com/panjf2000/gnet.(*EventServer).OnClosed g: ->github.com/panjf2000/gnet.(*conn).releaseTCP g: ->github.com/panjf2000/gnet/pool/ringbuffer.Put g: ->github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g: ->github.com/panjf2000/gnet/pool/ringbuffer.index g: <-github.com/panjf2000/gnet/pool/ringbuffer.index g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g: <-github.com/panjf2000/gnet/pool/ringbuffer.Put g: ->github.com/panjf2000/gnet/pool/ringbuffer.Put g: ->github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g: ->github.com/panjf2000/gnet/pool/ringbuffer.index g: <-github.com/panjf2000/gnet/pool/ringbuffer.index g: ->github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g: <-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g: <-github.com/panjf2000/gnet/pool/ringbuffer.Put g: <-github.com/panjf2000/gnet.(*conn).releaseTCP g: <-github.com/panjf2000/gnet.(*eventloop).loopCloseConn g: <-github.com/panjf2000/gnet.(*eventloop).loopRead g: ->github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g: <-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink 通过gnet-demo输出,我们可以清晰看到gnet接收一个连接,在这个连接上读写以及关闭这个连接的函数调用链,有了这个链条,我们再来阅读gnet源码就轻松许多了,即便有回调函数也没有问题。 上面输出的函数调用链的内容已经很多了。但如果你还不满足于这些,比如我还要跟踪到gnet依赖的golang.org/x/sys中,那可以利用相同思路,将golang.org/x/sys下载到本地,并通过functrace添加跟踪设施,并在gnet-demo中用replace换掉golang.org/x/sys,让其指向本地的sys包代码。如果觉得信息太多,可以通过gen命令做单个必要go源文件的跟踪信息添加,而不必要用批量方式。进一步的跟踪sys包的函数调用链的作业就留给大家了,这里就不深入了。 代码阅读完成后,我们只需在gnet目录下执行如下命令便可以恢复gnet原来的面貌:$git checkout .
------------------------- “Gopher部落”知识星球 正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦: * Go技术书籍的书摘和读书体会系列* Go与eBPF系列
欢迎大家加入! Go技术专栏“改善Go语⾔编程质量的50个有效实践 ”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订阅!
我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用 ”在慕课网热卖中,欢迎小伙伴们订阅学习!我爱发短信
:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5GRCS消息。
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1core
CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址 :https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 我的联系方式: * 微博:https://weibo.com/bigwhite20xx * 微信公众号:iamtonybai * 博客:tonybai.com * github: https://github.com/bigwhite*
“Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544微信赞赏:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。 通过实例理解GO逃逸分析* 五月 24, 2021
* 0 条评论
本文永久链接
–
https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example 翻看了一下自己的Go文章归档 ,发现自己从未专门写过有关Go逃逸分析(escape analysis)的文章。关于Go变量的逃逸分析,大多数Gopher其实并不用关心,甚至可以无视。但是如果你将Go应用于性能敏感的领域,要完全压榨出Go应用的性能,那么理解Go逃逸分析就大有裨益了。在本文,我们就一起来理解一下Go的逃逸分析。 1. 逃逸分析(ESCAPE ANALYSIS)要解决的问题 C/C++语言出身的程序员 对堆内存(heap)和栈内存(stack)都有着“泾渭分明”的理解。在操作系统演化出现进程虚拟内存地址(virtualmemory
address)的概念后,如下图所示,应用程序的虚拟内存地址空间就被划分为堆内存区(如图中的heap)和栈内存区(如图中的stack): 图:一个进程的虚拟内存地址空间(图来自https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast) 在x86平台linux操作系统下,如上图,一般将栈内存区放在高地址,栈向下延伸;而堆内存去放在低地址,堆向上延伸,这样做的好处就是便于堆和栈可动态共享那段内存区域。 > 这是否意味着所有分配在堆内存区域的内存对象地址一定比分配在栈内存区域的内存对象地址要小呢?在C/C++中是这样的,但是在Go语言中,这是不一定的,因为go堆内存所使用的内存页(page)与goroutine的栈所使用的内存页是交织在一起的> 。
无论是栈内存还是堆内存,对于应用而言都是合法可用的内存地址空间。之所以将其区分开,是因为应用程序的内存分配和管理的需要。 栈内存上的对象的存储空间是自动分配和销毁的,无需开发人员或编程语言运行时过多参与,比如下面的这段C代码(用C代码更能体现栈内存与堆内存的差别): // github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cstack.c#include
void bar() {
int e = 31;
int f = 32;
printf("e = %d\n", e); printf("f = %d\n", f);}
void foo() {
int c = 21;
int d = 22;
printf("c = %d\n", c); printf("d = %d\n", d);}
int main() {
int a = 11;
int b = 12;
printf("a = %d\n", a); printf("b = %d\n", b);foo();
bar();
}
上面这段c程序算上main函数共有三个函数,每个函数中都有两个整型变量,C编译器自动为这些变量在栈内存上分配空间,我们无需考虑它什么时候被创建以及何时被销毁,我们只需在特定的作用域(其所在函数内部)使用它即可,而无需担心其内存地址不合法,因此这些被分配在栈内存上的变量也被称为“自动变量”。但是如果将其地址返回到函数的外部,那么函数外部的代码通过解引用而访问这些变量时便会出错,如下面示例: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cstack_coredump.c#include
int *foo() {
int c = 11;
return &c;
}
int main() {
int *p = foo();
printf("the return value of foo = %d\n", *p);}
如代码所示,在上面这个例子中,我们将foo函数内的自动变量c的地址通过函数返回值返回给foo函数的调用者(main)了,这样当我们在main函数中引用该地址输出该变量值的时候,我们就会收到异常,比如在ubuntu上运行上述程序,我们会得到如下结果(在macos上运行,gcc会给出相同的警告,但程序运行不会dumpcore):
# gcc cstack_dumpcore.c cstack_dumpcore.c: In function ‘foo’: cstack_dumpcore.c:5:12: warning: function returns address of local variablereturn &c;
^~
# ./a.out
Segmentation fault (core dumped) 这样一来我们就需要一种内存对象,可以在全局(跨函数间)合法使用,这就是堆内存对象。但是和位于栈上的内存对象由程序自行创建销毁不同,堆内存对象需要通过专用API手工分配和释放,在C中对应的分配和释放方法就是malloc和free: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cheap.c#include
#include int *foo() {
int *c = malloc(sizeof(int));*c = 12;
return c;
}
int main() {
int *p = foo();
printf("the return value of foo = %d\n", *p);free(p);
}
在这个示例中我们使用malloc在foo函数中分配了一个堆内存对象,并将该对象返回给main函数,main函数使用完该对象后调用了free函数手工释放了该堆内存块。 显然和自动变量相比,堆内存对象的生命周期管理将会给开发人员带来很大的心智负担。为了降低这方面的心智负担,带有GC(垃圾回收)的编程语言出现了,比如Java、Go等。这些带有GC的编程语言会对位于堆上的对象进行自动管理。当某个对象不可达时(即没有其对象引用它时),它将会被回收并被重用。 但GC的出现虽然降低了开发人员在内存管理方面的心智负担,但GC不是免费的,它给程序带来的性能损耗是不可忽视的,尤其是当堆内存上有大量待扫描的堆内存对象时,将会给GC带来过大的压力,从而使得GC占用更多本应用于处理业务逻辑的计算和存储资源。于是人们开始想方法尽量减少在堆上的内存分配,可以在栈上分配的变量尽量留在栈上。 逃逸分析(ESCAPE ANALYSIS)就是在程序编译阶段根据程序代码中的数据流,对代码中哪些变量需要在栈上分配,哪些变量需要在堆上分配进行静态分析的方法。一个理想的逃逸分析算法自然是能将那些人们认为需要分配在栈上的变量尽可能保留在栈上,尽可能少的“逃逸”到堆上的算法。但这太过理想,各种语言都有自己的特殊情况,各种语言的逃逸算法的精确度实际都会受到这方面的影响。 2. GO语言的逃逸分析Go从诞生那天
起,逃逸分析就始终伴随其左右。正如上面说到的逃逸分析的目标,Go编译器使用逃逸分析来决定哪些变量应该在goroutine的栈上分配,哪些变量应该在堆上分配。 截至目前,Go一共有两个版本的逃逸分析实现,分水岭在Go1.13版本 。Go
1.13版本之前是Go逃逸分析的第一版实现,位于Go源码的src/cmd/compile/internal/gc/esc.go中(以go 1.12.7版本为例),代码规模2400多行;Go 1.13版本中加入了由Matthew Dempsky 重写的第二版逃逸分析 ,并默认开启,可以通过-gcflags=”-m -newescape=false”恢复到使用第一版逃逸分析。之所以重写,主要是考虑第一版代码的可读性和可维护性问题,新版代码主要位于Go项目源码的src/cmd/compile/internal/gc/escape.go中,它将逃逸分析代码从上一版的2400多行缩减为1600多行,并作了更为完整文档和注释。但注意的是新版代码在算法精确性上并没有质的变化。 但即便如此,经过了这么多年的“修修补补”,Dmitry Vyukov 2015年提出的那些“Go Escape Analysis Flaws” 多数已经fix了。Go项目中内置了对逃逸分析的详尽的测试代码(位于Go项目下的test/escape*.go文件中)。 在新版逃逸分析实现的注释中($GOROOT/src/cmd/compile/internal/gc/escape.go),我们可以大致了解逃逸分析的实现原理。注释中的原理说明中提到了算法基于的两个不变性: * 指向栈对象的指针不能存储在堆中(pointers to stack objects cannot be stored in the heap);*
指向栈对象的指针不能超过该栈对象的存活期(即指针不能在栈对象被销毁后依旧存活)(pointers to a stack object cannot outlive that object)。 源码注释中也给出Go逃逸分析的大致原理和过程。Go逃逸分析的输入是Go编译器解析了Go源文件后所获得的整个程序的抽象语法树(Abstract syntax tree,AST): 源码解析后得到的代码AST的Node切片为xtop: // $GOROOT/src/cmd/compile/internal/gc/go.govar xtop *Node
在Main函数中,xtop被传入逃逸分析的入口函数escapes: // $GOROOT/src/cmd/compile/internal/gc/main.go // Main parses flags and Go source files specified in the command-line // arguments, type-checks the parsed Go package, compiles functions to machine // code, and finally writes the compiled package definition to disk. func Main(archInit func(*Arch)) {... ...
// Phase 6: Escape analysis. // Required for moving heap allocations onto stack, // which in turn is required by the closure implementation, // which stores the addresses of stack variables into the closure. // If the closure does not escape, it needs to be on the stack // or else the stack copier will not update it. // Large values are also moved off stack in escape analysis; // because large values may contain pointers, it must happen early. timings.Start("fe", "escapes")escapes(xtop)
... ...
}
下面是escapes函数的实现: // $GOROOT/src/cmd/compile/internal/gc/esc.go func escapes(all *Node) { visitBottomUp(all, escapeFuncs)}
// $GOROOT/src/cmd/compile/internal/gc/scc.go // 强连接node - 一个数据结构 func visitBottomUp(list *Node, analyze func(list *Node, recursive bool)) { var v bottomUpVisitor v.analyze = analyze v.nodeID = make(mapuint32) for _, n := range list { if n.Op == ODCLFUNC && !n.Func.IsHiddenClosure() {v.visit(n)
}
}
}
// $GOROOT/src/cmd/compile/internal/gc/escape.go // escapeFuncs performs escape analysis on a minimal batch of// functions.
func escapeFuncs(fns *Node, recursive bool) { for _, fn := range fns { if fn.Op != ODCLFUNC { Fatalf("unexpected node: %v", fn)}
}
var e Escape
e.heapLoc.escapes = true // Construct data-flow graph from syntax trees. for _, fn := range fns {e.initFunc(fn)
}
for _, fn := range fns {e.walkFunc(fn)
}
e.curfn = nil
e.walkAll()
e.finish(fns)
}
根据注释,escapes的大致原理是(直译):*
首先,构建一个有向加权图,其中顶点(称为”location”,由gc.EscLocation表示)代表由语句和表达式分配的变量,而边(gc.EscEdge)代表变量之间的赋值(权重代表寻址/取地址次数)。*
接下来,遍历(visitBottomUp)该有向加权图,在图中寻找可能违反上述两个不变量条件的赋值路径。 违反上述不变量的赋值路径。如果一个变量v的地址是储存在堆或其他可能会超过它的存活期的地方,那么v就会被标记为需要在堆上分配。*
为了支持函数间的分析,算法还记录了从每个函数的参数到堆的数据流以及到其结果的数据流。算法将这些信息称为“参数标签(parameter tag)”。这些标签信息在静态调用时使用,以改善对函数参数的逃逸分析。 当然即便看到这,你可能依旧一头雾水,没关系,这里不是讲解逃逸分析原理,如果想了解原理,那就请认真阅读那2400多行代码。 > 注:有一点需要明确,那就是静态逃逸分析也无法确定的对象会被放置在堆上,后续精确的GC会处理这些对象,这样最大程度保证了代码的安全性。 3. GO逃逸分析的示例 Go工具链提供了查看逃逸分析过程的方法,我们可以通过在-gcflags中使用-m来让Go编译器输出逃逸分析的过程,下面是一些典型的示例。 1) 简单原生类型变量的逃逸分析 我们来看一个原生整型变量的逃逸分析过程,下面是示例的代码: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/int.go1 package main
2
3 import "testing"4
5 func foo() {
6 a := 11 7 p := new(int) 8 *p = 12 9 println("addr of a is", &a) 10 println("addr that p point to is", p)11 }
12
13 func bar() (*int, *int) { 14 m := 21 15 n := 22 16 println("addr of m is", &m) 17 println("addr of n is", &n) 18 return &m, &n19 }
20
21 func main() { 22 println(int(testing.AllocsPerRun(1, foo))) 23 println(int(testing.AllocsPerRun(1, func() { 24 bar()25 })))
26 }
我们通过-gcflags “-m -l”来执行逃逸分析,之所以传入-l是为了关闭inline,屏蔽掉inline对这个过程以及最终代码生成的影响: // go 1.16版本 on MacOS $go build -gcflags "-m -l" int.go # command-line-arguments ./int.go:7:10: new(int) does not escape ./int.go:14:2: moved to heap: m ./int.go:15:2: moved to heap: n ./int.go:23:38: func literal does not escape 逃逸分析的结果与我们手工分析的一致:函数bar中的m、n逃逸到heap(对应上面输出的有movedto heap:
xx字样的行),这两个变量将在heap上被分配存储空间。而函数foo中的a以及指针p指向的内存块都在栈上分配(即便我们是调用的new创建的int对象,Go中new出来的对象可不一定分配在堆上,逃逸分析的输出日志中还专门提及new(int)没有逃逸)。我们执行一下该示例(执行时同样传入-l关闭inline): $go run -gcflags "-l" int.go addr of a is 0xc000074860 addr that p point to is 0xc000074868 addr of a is 0xc000074860 addr that p point to is 0xc0000748680
addr of m is 0xc0000160e0 addr of n is 0xc0000160e8 addr of m is 0xc0000160f0 addr of n is 0xc0000160f82
首先,我们看到未逃逸的a和p指向的内存块的地址区域在0xc000074860~0xc000074868;而逃逸的m和n被分配到了堆内存空间,从输出的结果来看在0xc0000160e0~0xc0000160e8。我们可以明显看到这是两块不同的内存地址空间;另外通过testing包的AllocsPerRun的输出,我们同样印证了函数bar中执行了两次堆内存分配动作。 我们再来看看这个代码对应的汇编代码: $go tool compile -S int.go |grep new 0x002c 00044 (int.go:14) CALL runtime.newobject(SB) 0x004d 00077 (int.go:15) CALL runtime.newobject(SB) rel 45+4 t=8 runtime.newobject+0 rel 78+4 t=8 runtime.newobject+0 我们看到在对应源码的14和15行,汇编调用了runtime.newobject在堆上执行了内存分配动作,这恰是逃逸的m和n声明的位置。从下面newobject代码的实现我们也能看到,它实际上在gc管理的内存上执行了malloc动作: // $GOROOT/src/runtime/malloc.go // implementation of new builtin // compiler (both frontend and SSA backend) knows the signature // of this function func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true)}
2) 切片变量自身和切片元素的逃逸分析 了解过切片实现原理 的gopher都知道,切片变量实质上是一个三元组: //$GOROOT/src/runtime/slice.go type slice struct { array unsafe.Pointerlen int
cap int
}
其中这个三元组的第一个字段array指向的是切片底层真正存储元素的指针。这样当为一个切片变量分配内存时,便既要考虑切片本身(即上面的slice结构体)在哪里分配,也要考虑切片元素的存储在哪里分配。我们看下面示例: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/slice.go1 package main
2
3 import (
4 "reflect" 5 "unsafe"6 )
7
8 func noEscapeSliceWithDataInHeap() { 9 var sl int 10 println("addr of local(noescape, data in heap) slice = ", &sl) 11 printSliceHeader(&sl) 12 sl = append(sl, 1) 13 println("append 1") 14 printSliceHeader(&sl) 15 println("append 2") 16 sl = append(sl, 2) 17 printSliceHeader(&sl) 18 println("append 3") 19 sl = append(sl, 3) 20 printSliceHeader(&sl) 21 println("append 4") 22 sl = append(sl, 4) 23 printSliceHeader(&sl)24 }
25
26 func noEscapeSliceWithDataInStack() { 27 var sl = make(int, 0, 28 println("addr of local(noescape, data in stack) slice = ", &sl) 29 printSliceHeader(&sl) 30 sl = append(sl, 1) 31 println("append 1") 32 printSliceHeader(&sl) 33 sl = append(sl, 2) 34 println("append 2") 35 printSliceHeader(&sl)36 }
37
38 func escapeSlice() *int { 39 var sl = make(int, 0, 40 println("addr of local(escape) slice = ", &sl) 41 printSliceHeader(&sl) 42 sl = append(sl, 1) 43 println("append 1") 44 printSliceHeader(&sl) 45 sl = append(sl, 2) 46 println("append 2") 47 printSliceHeader(&sl) 48 return &sl49 }
50
51 func printSliceHeader(p *int) { 52 ph := (*reflect.SliceHeader)(unsafe.Pointer(p)) 53 println("slice data =", unsafe.Pointer(ph.Data))54 }
55
56 func main() { 57 noEscapeSliceWithDataInHeap() 58 noEscapeSliceWithDataInStack() 59 escapeSlice()60 }
对上述示例运行逃逸分析: $go build -gcflags "-m -l" slice.go # command-line-arguments ./slice.go:51:23: p does not escape ./slice.go:27:15: make(int, 0, does not escape ./slice.go:39:6: moved to heap: sl ./slice.go:39:15: make(int, 0, escapes to heap 我们从输出的信息中看到: * 位于39行的escapeSlice函数中的sl逃逸到堆上了;*
位于39行的escapeSlice函数中的切片sl的元素也逃逸到堆上了; * 位于27行的切片sl的元素没有逃逸。 由于很难看到三个函数中各个切片的元素是否逃逸,我们通过运行该示例来看一下: $go run -gcflags " -l" slice.go addr of local(noescape, data in heap) slice = 0xc00006af48slice data = 0x0
append 1
slice data = 0xc0000160c0append 2
slice data = 0xc0000160d0append 3
slice data = 0xc0000140c0append 4
slice data = 0xc0000140c0 addr of local(noescape, data in stack) slice = 0xc00006af48 slice data = 0xc00006af08append 1
slice data = 0xc00006af08append 2
slice data = 0xc00006af08 addr of local(escape) slice = 0xc00000c030 slice data = 0xc00001a100append 1
slice data = 0xc00001a100append 2
slice data = 0xc00001a100 > 注:我们利用reflect包的SliceHeader输出切片三元组中的代表底层数组地址的字段,这里是slice> data。
我们看到:
*
第一个函数noEscapeWithDataInHeap声明了一个空slice,并在后面使用append向切片附加元素。从输出结果来看,slice自身是分配在栈上的,但是运行时在动态扩展切片时,选择了将其元素存储在heap上;*
第二个函数noEscapeWithDataInStack直接初始化了一个包含8个元素存储空间的切片,切片自身没有逃逸,并且在附加(append)的元素个数小于等于8个的时候,元素直接使用了为其分配的栈空间;但如果附加的元素超过8个,那么运行时会在堆上分配一个更大的空间并将原栈上的8个元素复制过去,后续该切片的元素就都存储在了堆上。这也是为什么强烈建议在创建 slice 时带上预估的cap参数的原 ,不仅减少了堆内存的频繁分配,在切片变量未逃逸的情况下,在cap容量之下,所有元素都分配在栈上,这将提升运行性能。*
第三个函数escapeSlice则是切片变量自身以及其元素的存储都在堆上。 3) FMT.PRINTF系列函数让变量逃逸到堆(HEAP)上了? 很多人在go项目的issue中反馈fmt.Printf系列函数让变量逃逸到堆上了,情况真的是这样么?我们通过下面示例来看一下: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf1.go1 package main
2
3 import "fmt"
4
5 func foo() {
6 var a int = 66666666 7 var b int = 77 8 fmt.Printf("a = %d\n", a) 9 println("addr of a in foo =", &a) 10 println("addr of b in foo =", &b)11 }
12
13 func main() {14 foo()
15 }
> 注:println和print两个预定义函数并没有像fmt.Printf系列函数的“副作用”,不会影响变量的逃逸性。所以这里使用println来输出变量的实际分配内存地址。 对上面的代码运行逃逸分析: $go build -gcflags "-m -l" printf1.go # command-line-arguments ./printf1.go:8:12: ... argument does not escape ./printf1.go:8:13: a escapes to heap 我们看到逃逸分析输出第8行的变量“a escapes to heap”,不过这个“逃逸”有些奇怪,因为按照之前的经验,如果某个变量真实逃逸了,那么逃逸分析会在其声明的那行输出:“movedto heap:
xx”字样。而上面这个输出既不是在变量声明的那一行,也没有输出“movedto heap:
a”字样,变量a真的逃逸了么?我们运行一下上面示例,看看变量a的地址究竟是在堆上还是栈上: $go run -gcflags "-l" printf1.goa = 66666666
addr of a in foo = 0xc000092f50 addr of b in foo = 0xc000092f48 我们看到变量a的地址与未逃逸的变量b的地址都在同一个栈空间,变量a并未逃逸!如果你反编译为汇编,你肯定也看不到runtime.newobject的调用。 那么“./printf1.go:8:13: a escapes to heap”这句的含义究竟是什么呢?显然逃逸分析在这一行是对进入fmt.Printf的数据流的分析,我们修改一下go标准库源码,然后build -a重新编译一下printf1.go ,看看在fmt.Printf内部变量的分布情况: // $GOROOT/src/fmt/print.go func Printf(format string, a ...interface{}) (n int, err error) { // 添加下面四行代码 for i := 0; i < len(a); i++ {println(a)
println(&a)
}
return Fprintf(os.Stdout, format, a...)}
重新编译printf1.go并运行编译后的可执行文件(为了避免): $go build -a -gcflags "-l" printf1.go$./printf1
(0x10af200,0xc0000160c8)0xc00006cf58
a = 66666666
addr of a in foo = 0xc00006cf50 addr of b in foo = 0xc00006cf48 我们看到fmt.Printf的实参a在传入后被装箱到一个interface{}类型的形参变量中,而这个形参变量自身则是被分配在栈上的(0xc00006cf58),而通过println输出的该interface{}类型形参变量的类型部分和值部分分别指向0x10af200和0xc0000160c8。显然值部分是在堆内存上分配的。那么“./printf1.go:8:13:a escapes to
heap”是否指的是装箱后的值部分在堆上分配呢?这里也不确定。 我们再来看一个例子来对比一下: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf2.go1 package main
2
3 import "fmt"
4
5 func foo() {
6 var a int = 66666666 7 var b int = 77 8 fmt.Printf("addr of a in bar = %p\n", &a) 9 println("addr of a in bar =", &a) 10 println("addr of b in bar =", &b)11 }
12
13 func main() {14 foo()
15 }
在printf2.go这个例子中,与printf1.go不同的是我们在foo函数中使用fmt.Printf输出的是变量a的地址:&a。我们运行一下新版逃逸分析:// go 1.16
$go build -gcflags "-m -l" printf2.go # command-line-arguments ./printf2.go:6:6: moved to heap: a ./printf2.go:8:12: ... argument does not escape 我们看到位于第6行声明的变量a居然真的逃逸到了堆上。我们运行一下printf2.go: $go build -a -gcflags "-l" printf2.go$./printf2
(0x10ab4a0,0xc0000160c8)0xc00006cf58
addr of a in bar = 0xc0000160c8 addr of a in bar = 0xc0000160c8 addr of b in bar = 0xc00006cf48 我们看到变量a的地址果然与位于栈上的变量b相差很大,应该就是在堆上,那么这样看那些在go项目中提issue的gopher所言不虚。变量a的地址以实参的形式传入fmt.Printf后被装箱到一个interface{}形参变量中,而从结果来看,fmt.Printf真的要求装箱的形参变量的值部分要在堆上分配,但根据逃逸分析不变性,堆上的对象不能存储一个栈上的地址,而这次存储的是a的地址,于是将a判定为逃逸,于是a自身也就被分配到了堆上(0xc0000160c8)。 我们用go 1.12.7运行一下老版的逃逸分析:// go 1.12.7
$go build -gcflags "-m -l" printf2.go # command-line-arguments ./printf2.go:8:40: &a escapes to heap ./printf2.go:8:40: &a escapes to heap ./printf2.go:6:6: moved to heap: a ./printf2.go:8:12: foo ... argument does not escape ./printf2.go:9:32: foo &a does not escape ./printf2.go:10:32: foo &b does not escape 老版的逃逸分析给出了更详细的输出,比如:“&aescapes to
heap”,其所指想必就是&a被装箱到堆内存上;而println输出&a则无需&a被装箱。但此后对变量a的最终判定为逃逸。 > Go核心团队成员Keith Randall > 对逃逸分析输出的日志给过一个解释 > ,大致意思是:当逃逸分析输出“b> escapes to
> heap”时,意思是指存储在b中的值逃逸到堆上了(当b为指针变量时才有意义),即任何被b引用的对象必须分配在堆上,而b自身则不需要;如果b自身也逃逸到堆上,那么逃逸分析会输出“&b > escapes to heap”。 这个问题目前已经没有fix,其核心问题在8618这个issue中。
5. 手动强制避免逃逸 对于printf2.go中的例子,我们确定一定以及肯定:a不需要逃逸。但若使用fmt.Printf,我们无法阻拦a的逃逸。那是否有一种方法可以干扰逃逸分析,使逃逸分析认为需要在堆上分配的内存对象而我们确定认为不需要逃逸的对象避免逃逸呢?在Go运行时代码中,我们发现了一个函数: // $GOROOT/src/runtime/stubs.go func noescape(p unsafe.Pointer) unsafe.Pointer {x := uintptr(p)
return unsafe.Pointer(x ^ 0) // 任何数值与0的异或都是原数}
并且在Go标准库和运行时实现中,该函数得到大量使用。该函数的实现逻辑使得我们传入的指针值与其返回的指针值是一样的。该函数只是通过uintptr做了一次转换,而这次转换将指针转换成了数值,这“切断”了逃逸分析的数据流跟踪,导致传入的指针避免逃逸。 我们看一下下面例子: // github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf3.gopackage main
import (
"fmt"
"unsafe"
)
func noescape(p unsafe.Pointer) unsafe.Pointer {x := uintptr(p)
return unsafe.Pointer(x ^ 0)}
func foo() {
var a int = 66666666var b int = 77
fmt.Printf("addr of a in bar = %p\n", (*int)(noescape(unsafe.Pointer(&a)))) println("addr of a in bar =", &a) println("addr of b in bar =", &b)}
func main() {
foo()
}
对该代码实施统一分析: $go build -gcflags "-m -l" printf3.go # command-line-arguments ./printf3.go:8:15: p does not escape ./printf3.go:16:12: ... argument does not escape 我们看到a这次没有逃逸。运行一下编译后的可执行文件:$./printf3
(0x10ab4c0,0xc00009af50)0xc00009af58
addr of a in bar = 0xc00009af50 addr of a in bar = 0xc00009af50 addr of b in bar = 0xc00009af48 我们看到a没有像printf2.go那样被放在堆上,这次和b一样都是在栈上分配的。并且在fmt.Printf执行的过程中a的栈地址始终是有效的。 曾有一篇通过逃逸分析优化性能的论文《Escape
from Escape Analysis of Golang》使用的就是上述noescape函数的思路,有兴趣的童鞋可以自行下载阅读。6. 小结
通过这篇文章,我们了解到了逃逸分析要解决的问题、Go逃逸分析的现状与简单原理、一些Go逃逸分析的实例以及对逃逸分析输出日志的说明。最后,我们给出一个强制避开逃逸分析的方案,但要谨慎使用。 日常go开发过程,绝大多数情况无需考虑逃逸分析,除非性能敏感的领域。在这些领域,对系统执行热点路径做一次逃逸分析以及相应的优化,可能回带来程序性能的一定提升。 本文涉及的源码可以在这里 下载:https://github.com/bigwhite/experiments/blob/master/go-escape-analysis ------------------------- “Gopher部落”知识星球 正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦: * Go技术书籍的书摘和读书体会系列* Go与eBPF系列
欢迎大家加入! Go技术专栏“改善Go语⾔编程质量的50个有效实践 ”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订阅! 我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用 ”在慕课网热卖中,欢迎小伙伴们订阅学习!我爱发短信
:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5GRCS消息。
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1core
CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址 :https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 我的联系方式: * 微博:https://weibo.com/bigwhite20xx * 微信公众号:iamtonybai * 博客:tonybai.com * github: https://github.com/bigwhite*
“Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544微信赞赏:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。* 1
* 2
* 3
* 4
* 5
* 6
* ...
* Next »
搜索
如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 商务合作请联系bigwhite.cn AT aliyun.com 欢迎使用邮件订阅我的博客 输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!名字:
邮箱:
这里是 Tony Bai
的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片。 如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐,加油后的Tony
Bai将会为您呈现更多精彩的文章,谢谢! 如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码: 如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:比特币:
以太币:
如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^: 本站Powered by Digital Ocean VPS。 选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获
得。阿里云推荐码: 1WFZ0V, 立享9折!我的业余项目
* smspush短信发送平台文
* 使用functrace辅助进行Go项目源码分析 * 通过实例理解Go逃逸分析 * minikube v1.20.0版本的一个bug * Go标准库http与fasthttp服务端性能比较 * 使用reflect包在反射世界里读写各类型变量 * 给expvarmon插上数据持久化的“翅膀” * Go标准库flag包的“小陷阱” * Go语言“十诫” * Go泛型语法又出“幺蛾子”:引入type set概念和移除type list中的type关键字 * http.Client的连接行为控制详解评论
*
bigwhite 在
Go语言的“黑暗角落”:盘点学习Go语言时遇到的那些陷阱(第二部分) 感谢,我检查一下。*
delphier 在
Go语言的“黑暗角落”:盘点学习Go语言时遇到的那些陷阱(第二部分) GET https://secure.gravatar.com/avatar/157bf082b86...*
雄哼哼 在 通过实例深入理解sync.Map的工作原理 而且写的时候,明显是对整个map加锁的为什么会适合,多个goroutine读/写/修改的key集合没...*
雄哼哼 在 通过实例深入理解sync.Map的工作原理 “另外一种多个goroutine读/写/修改的key集合没有交集。”这个点没有解释把?*
Muyinliu 在 Hello,Termux 实际测试并不能用 QQ 拼音输入法在 Termux 里面使用蓝牙键盘直接输入中文,只能在 Termu...*
Muyinliu 在 Hello,Termux SwiftKey 可能是目前唯一一个在 Termux 里面使用外接键盘能输入中文的输入法,但是 Sw...*
bigwhite 在 defer函数参数求值简要分析 ok。欢迎多多交流。*
husy 在 defer函数参数求值简要分析 我搞错了,不好意思。多谢你的博客。*
husy 在 defer函数参数求值简要分析 冒昧说一句,现在go1.16.2的版本上执行,其结果是result=> 4 。
*
bigwhite 在 可视化Go内存管理 $cat -n test6.go 1package main 2 3impo...*
Details
Copyright © 2024 ArchiveBay.com. All rights reserved. Terms of Use | Privacy Policy | DMCA | 2021 | Feedback | Advertising | RSS 2.0