-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathen.search-data.min.8b0961a4c93a431f96e9ae0587ec4c3eecbcb2fe75dc341c3272ceeb84891060.json
More file actions
1 lines (1 loc) · 233 KB
/
en.search-data.min.8b0961a4c93a431f96e9ae0587ec4c3eecbcb2fe75dc341c3272ceeb84891060.json
File metadata and controls
1 lines (1 loc) · 233 KB
1
[{"id":0,"href":"/1.base/1-2-hello-world/","title":"可热更新的定时器","section":"1.bases","content":"1.2.1 hello world # Go 语言是谷歌 2009 发布的第二款开源编程语言。\nGo 语言专门针对多处理器系统应用程序的编程进行了优化,使用 Go 编译的程序可以媲美 C 或 C++代码的速度,而且更加安全、支持并行进程。\n1.2.1 为什么要选择学习 Go 语言呢?与其他语言的应用相比,它有什么优点呢? # 1、学习曲线它包含了类 C 语法、GC 内置和工程工具。这一点非常重要,因为 Go 语言容易学习,所以一个普通的大学生花一个星期就能写出来可以上手的、高性能的应用。在国内大家都追求快,这也是为什么国内 Go 流行的原因之一。\n2、效率 Go 拥有接近 C 的运行效率和接近 PHP 的开发效率,这就很有利的支撑了上面大家追求快速的需求。\n3、出身名门、血统纯正之所以说 Go 语言出身名门,是因为我们知道 Go 语言出自 Google 公司,这个公司在业界的知名度和实力自然不用多说。Google 公司聚集了一批牛人,在各种编程语言称雄争霸的局面下推出新的编程语言,自然有它的战略考虑。而且从 Go 语言的发展态势来看,Google 对它这个新的宠儿还是很看重的,Go 自然有一个良好的发展前途。我们看看 Go 语言的主要创造者,血统纯正这点就可见端倪了。\n4、自由高效:组合的思想、无侵入式的接口 Go 语言可以说是开发效率和运行效率二者的完美融合,天生的并发编程支持。Go 语言支持当前所有的编程范式,包括过程式编程、面向对象编程以及函数式编程。程序员们可以各取所需、自由组合、想怎么玩就怎么玩。\n5、强大的标准库这包括互联网应用、系统编程和网络编程。Go 里面的标准库基本上已经是非常稳定了,特别是我这里提到的三个,网络层、系统层的库非常实用。\n6、部署方便:二进制文件、Copy 部署我相信这一点是很多人选择 Go 的最大理由,因为部署太方便了,所以现在也有很多人用 Go 开发运维程序。\n7、简单的并发它包含了降低心智的并发和简易的数据同步,我觉得这是 Go 最大的特色。之所以写正确的并发、容错和可扩展的程序如此之难,是因为我们用了错误的工具和错误的抽象,Go 可以说这一块做的相当简单。\n8、稳定性 Go 拥有强大的编译检查、严格的编码规范和完整的软件生命周期工具,具有很强的稳定性,稳定压倒一切。那么为什么 Go 相比于其他程序会更稳定呢?这是因为 Go 提供了软件生命周期(开发、测试、部署、维护等等)的各个环节的工具,如 go tool、gofmt、go test。\n1.2.2 Go 语言适合用来做什么? # 服务器编程:以前你如果使用 C 或者 C++做的那些事情,用 Go 来做很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。\n分布式系统:数据库代理器等。\n网络编程:这一块目前应用最广,包括 Web 应用、API 应用、下载应用、内存数据库。\n云平台:google 开发的 groupcache,couchbase 的部分组建云平台,目前国外很多云平台在采用 Go 开发,CloudFoundy 的部分组建,前 VMare 的技术总监自己出来搞的 apcera 云平台。\n1.2.3 Go 语言成功的项目 # nsq:bitly 开源的消息队列系统,性能非常高,目前他们每天处理数十亿条的消息\ndocker:基于 lxc 的一个虚拟打包工具,能够实现 PAAS 平台的组建\npacker:用来生成不同平台的镜像文件,例如 VM、vbox、AWS 等,作者是 vagrant 的作者\nskynet:分布式调度框架\nDoozer:分布式同步工具,类似 ZooKeeper\nHeka:mazila 开源的日志处理系统\ncbfs:couchbase 开源的分布式文件系统\ntsuru:开源的 PAAS 平台,和 SAE 实现的功能一模一样\ngroupcache:memcahe 作者写的用于 Google 下载系统的缓存系统\ngod:类似 redis 的缓存系统,但是支持分布式和扩展性\ngor:网络流量抓包和重放工具\n1.2.4 哪些大公司在用 go 语言? # Google\n这个不用多做介绍,作为开发 Go 语言的公司,当仁不让。Google 基于 Go 有很多优秀的项目,比如:https://github.com/kubernetes/kubernetes ,大家也可以在 Github 上 https://github.com/google/ 查看更多 Google 的 Go 开源项目。\nFacebook\nFacebook 也在用,为此他们还专门在 Github 上建立了一个开源组织 facebookgo,大家可以通过 https://github.com/facebookgo 访问查看 facebook 开源的项目,比如著名的是平滑升级的 grace。腾讯\n腾讯作为国内的大公司,还是敢于尝试的,尤其是 Docker 容器化这一块,他们在 15 年已经做了 docker 万台规模的实践,具体可以参考 http://www.infoq.com/cn/articles/tencent-millions-scale-docker-application-practice\n百度\n目前所知的百度的使用是在运维这边,是百度运维的一个 BFE 项目,负责前端流量的接入。他们的负责人在 2016 年有分享,大家可以看下这个 http://www.infoq.com/cn/presentations/application-of-golang-in-baidu-frontend\n阿里\n阿里巴巴具体的项目不太清楚,不过听说其系统部门、CDN 等正在招 Go 方面的人。京东\n京东云消息推送系统、云存储,以及京东商城等都有使用 Go 做开发。\n小米\n小米对 Golang 的支持,莫过于运维监控系统的开源,也就是 http://open-falcon.com/\n此外,小米互娱、小米商城、小米视频、小米生态链等团队都在使用Golang。\n360\n360 对 Golang 的使用也不少,一个是开源的日志搜索系统 Poseidon,托管在 Github 上,https://github.com/Qihoo360/poseidon\nGo语言前景:\n (以上数据来源于网络)\n1.2.5 第一个 go 程序 # 带着目标学东西往往是最有成效的,为什么学以及环境安装可以参考前面的文章。\nlet\u0026rsquo;s go go go !\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, World!\u0026#34;) } 这是一个最简单的 go 程序,由三个元素构成\n元素一、包 :package是包,每个文件夹是一个包,默认包名就是文件夹名,文件夹下所有的.go文件全部都是同一个包。\nmain包整个项目的入口,你可以在指定任意一个文件夹当作程序的入口,然后把里面所有文件第一行改成package main,也可以把项目根目录当作main包。\nPS: 每个项目默认有且只有一个main包\n元素二、import:这个关键字代表引入其他地方的包,可以是当前项目的,也可以是别人写的。 这里import \u0026quot;fmt\u0026quot;引入的就是 go 原生的fmt包,专门用来输出文本的。\n元素三、语句\nfunc main() { fmt.Println(\u0026#34;Hello, World!\u0026#34;) } main 入口函数,每个项目只有一个main函数\n执行以上代码输出\n$ go run hello.go Hello, World! 1.2.6 让你的项目在IDE里跑起来 # 每次新建项目,不熟悉go的项目结构,一般跑都跑不起来,每次都要重新搞一遍,好几回跑项目都会报类似File is invalid的错误\n 有时候报其他奇怪的错误,今天就下决心整理一下,理一理概念 GOROOT、GOPATH、src、 pkg、bin,希望以后不要再出现这样的问题了,同时给看到文章的你一些帮助。\n1.2.6.1 熟悉golang项目目录结构 # 要想让你的程序跑起来,要按照这样的目录结构,正常情况下有三个目录:\n|--bin |--pkg |--src 其中,bin存放编译后的可执行文件;pkg存放编译后的包文件;src存放项目源文件。一般,bin和pkg目录可以不创建,go命令会自动创建(爽否?),只需要创建src目录放代码即可。\n我创建一个src目录,下面再创建一个叫main的项目(可以叫任何名字,我只是示例叫main),里面只有一个main.go文件。\n 他的内容是:\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello world\u0026#34;) } 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.2-hello-world\n 这样一个简单的项目就创建好了,创建好只是第一步,下面让她跑起来。\n1.2.6.2 让她跑起来 # 找到配置,Goland里面大多数的配置都在这里。\n 配置你的GOROOT,配置成你安装的go路径,Goland会自动识别,这就是GOROOT的作用,和JAVA_HOME的作用差不多。\n 配置GOPATH,你的项目放在src下面不是随随便便就放的,得让go知道你这些个项目基于哪个位置。\n 细心的人注意到,这里有一个Project GOPATH,还有一个Global GOPATH,把你的项目配置在Project GOPATH里,每个项目都不一样,创建另一个项目时这个路径要配置成新项目的。\nGlobal GOPATH可以弄一个公共项目,以后就把第三方的包直接装到这里,就可以自动在你的项目里引用了。\n调出ToolBar,开始配置运行文件\n 在ToolBar中Add Configuration\n 创建一个go build,可以看到有一个go remote的选项,它是用来调试远程服务器上的代码的,有兴趣关注我,我后续更新。\n 注意这三个位置,\n选File,运行文件就选main函数所在在文件main.go,输出文件夹就在和src同级目录的bin文件夹(自动创建),Working directory目录就是刚刚设置GOPATH的目录(自动)\n 注意,如果你多次打开目录选择,框框里的目录不会被替换掉,而是追加,导致运行的时候报错,除非你想一次性编译多个项目。\n例如这样:\n/Users/pzqu/Documents/code/go/what_go/src/main/main.go|/Users/pzqu/Documents/code/go/what_go/src/main/main.go 点击OK保存,之后,在ToolBar上点击运行,旁边那个符号是debug\n 成功运行!自动创建了bin目录\n 如果你想改输出的二进制文件名,可以在这里添加参数-o bin/main\n 1.2.7 如何在一个项目中使用其他项目? # 1.2.7.1 引用自己的项目中的其他模块包 # 写一个新函数func Add(a, b int) int,放在src下面main项目,calc文件夹,add.go文件里\n|____src | |____main | | |____calc | | | |____add.go | | |____main.go 代码如下\npackage calc func Add(a, b int) int { return a + b } 在main函数中调用他\n 输出结果:\n 几个点需要注意:\n add.go中的Add函数名首字母必须大写, 比如Add, Addxxx.只有大写的才是Public权限,外面的包才能访问,否则只能自己文件夹下代码才能访问\n add.go的改名为addyyy.go也可以,查找add包的时候,并不会根据add.go这个文件名来查找。而是根据文件夹名来查找,一个文件夹下的所有文件都属于同一个包。所以函数变量自然不能重复。\n main中调用add.Add(1,2)时,add是包, 必须跟add.go中的package处的包名一致,否则报错。\n import后, 怎么去查找对应的包呢? 思考一下, 很简单,无非就是GOROOT和GOPATH. 也应该明白了, src这个目录名可不是能随便取的。\n 1.2.7.2 引用第三方项目 # 自己写的其他项目引入,比如我这有一个叫common的公共包,你的公司有可能把很多go包下载下来,做一个公共仓库,方便公司内网隔离。\n 代码很简单\npackage dance import \u0026#34;fmt\u0026#34; func WhoDance() { fmt.Println(\u0026#34;you\u0026#34;) } 在main里面调用\npackage main import \u0026#34;common/dance\u0026#34; func main() { dance.WhoDance() } 输出\nyou Process finished with exit code 0 还有一个相当好用的引用第三方项目的工具,vendor关注我的博客,我们后续再见。\n1.2.8 小结 # 通过这一节,你已经了解到了go语言的历史和前景,并了解到怎么在IDE里跑起来go项目。这是一切的开始,算是进入了go语言的大门,在接下来的日子希望我们可以愉快的走下去。\n1.2.9 参考 # 小议并实战go包\u0026mdash;\u0026mdash;顺便说说go中的GOROOT,GOPATH和src,pkg,bin\n"},{"id":1,"href":"/1.base/1-1-install-download/","title":"1 1 Install Download","section":"1.bases","content":"1.1 安装和下载 # 1.1.1 下载位置 # go语言中文网\n1.1.2 如何安装 # 为你的系统下载了相应的安装包后,请按照 安装说明 进行安装。\n如果你选择从源码构建,请参考 从源码进行安装 。\n查看 发布历史 了解更多关于 Go 各版本的发布说明。\n1.1.3 小结 # 安装其实很容易,如果出现什么安装问题欢迎留言\n"},{"id":2,"href":"/1.base/1-3-go-mod/","title":"1 3 Go Mod","section":"1.bases","content":"1.3 go mod最佳实践 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.3-go-mod\n java 里有一个叫 maven 的包管理工具, go 也有一个叫 go mod 的管理工具,可以管理项目引用的第三方包版本、自动识别项目中用到的包、自动下载和管理包。\n为什么要使用go mod?\n 使用go mod仓库中可以不用再上传依赖代码包,防止代码仓库过大浪费以及多个项目同时用包时的浪费 可以管理引用包的版本,这一点是gopath(src模式)和vendor做不到的 如果依赖gopath不同项目如果引用了同一个软件包的不同版本,就会造成编译麻烦 gopath是go之前的默认策略,每个项目在运行时都要严格放在src目录下,而go mod不用\n原来的包管理方式\n 在不使用额外的工具的情况下,Go 的依赖包需要手工下载, 第三方包没有版本的概念,如果第三方包的作者做了不兼容升级,会让开发者很难受 协作开发时,需要统一各个开发成员本地$GOPATH/src下的依赖包 引用的包引用了已经转移的包,而作者没改的话,需要自己修改引用。 第三方包和自己的包的源码都在src下,很混乱。对于混合技术栈的项目来说,目录的存放会有一些问题 新的包管理模式解决了以上问题\n 自动下载依赖包 项目不必放在$GOPATH/src内了 项目内会生成一个go.mod文件,列出包依赖 所以来的第三方包会准确的指定版本号 对于已经转移的包,可以用 replace 申明替换,不需要改代码 1.3.1 配置 # golang\u0026gt;=1.12 添加环境变量 GO111MODULE 为 on 或者 auto ,设置方法\ngo env GO111MODULE=on go env -w GO111MODULE=\u0026#34;on\u0026#34; go env -w GOPROXY=https://goproxy.io go mod init 项目名 go mod tidy #add missing and remove unused modules 打开go mod 模式 使用国内下载包代理 初始化mod项目 自动增加包和删除无用包到 GOPATH 目录下(build的时候也会自动下载包加入到go.mod里面的) 注意:只要在本地设置一个公用path目录就可以了,全部的包都会下载到那里,其他本地项目用到时就可以共享了\n自动生成了go.mod和go.sum文件,可以不用理会,下面是简单介绍\n1.3.2 go.mod 文件 # go.mod 的内容比较容易理解\n 第一行:模块的引用路径 第二行:项目使用的 go 版本 第三行:项目所需的直接依赖包及其版本 在实际应用上,你会看见更复杂的 go.mod 文件,比如下面这样\nmodule github.com/BingmingWong/module-test go 1.14 require ( example.com/apple v0.1.2 example.com/banana v1.2.3 example.com/banana/v2 v2.3.4 example.com/pear // indirect example.com/strawberry // incompatible ) exclude example.com/banana v1.2.4 replace( golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac = \u0026gt; github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac golang.org/x/net v0.0.0-20180821023952-922f4815f713 = \u0026gt; github.com/golang/net v0.0.0-20180826012351-8a410e7b638d golang.org/x/text v0.3.0 = \u0026gt; github.com/golang/text v0.3.0 ) 主要是多出了两个 flag:\n exclude:忽略指定版本的依赖包 replace:由于在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。 1.3.3 go.sum 文件 # 每一行都是由 模块路径,模块版本,哈希检验值 组成,其中哈希检验值是用来保证当前缓存的模块不会被篡改。hash 是以h1:开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。\n值得注意的是,为什么有的包只有一行\n\u0026lt;module\u0026gt; \u0026lt;version\u0026gt;/go.mod \u0026lt;hash\u0026gt; 而有的包却有两行呢\n\u0026lt;module\u0026gt; \u0026lt;version\u0026gt; \u0026lt;hash\u0026gt; \u0026lt;module\u0026gt; \u0026lt;version\u0026gt;/go.mod \u0026lt;hash\u0026gt; 那些有两行的包,区别就在于 hash 值有两行,一行是 h1:hash 也就是模块包的hash,另一行是 go.mod h1:hash,举例如下\ngithub.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 而 h1:hash 和 go.mod h1:hash两者,要不就是同时存在,要不就是只存在 go.mod h1:hash。那什么情况下会不存在 h1:hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的h1 hash,就会出现不存在 h1 hash,只存在 go.mod h1:hash 的情况。\ngo.mod 和 go.sum 是 go modules 版本管理的指导性文件,因此 go.mod 和 go.sum 文件都应该提交到你的 Git 仓库中去,避免其他人使用你写项目时,重新生成的go.mod 和 go.sum 与你开发的基准版本的不一致。\n1.3.4 go mod 命令的使用 # go mod init:初始化go mod, 生成go.mod文件,后可接参数指定 module 名,上面已经演示过。\ngo mod download:手动触发下载依赖包到本地cache(默认为$GOPATH/pkg/mod目录)\ngo mod graph:打印项目的模块依赖结构\ngo mod tidy :添加缺少的包,且删除无用的包\ngo mod verify :校验模块是否被篡改过\ngo mod why:查看为什么需要依赖\ngo mod vendor :导出项目所有依赖到vendor下\n写入go.mod有两种方法:\n 你只要在项目中有 import 并使用或者使用下划线强制占用,然后 go build 就会 go module 就会自动下载并添加。 go mod tidy 1.3.5 vendor是什么 # vendor是项目缓存,为了防止开源代码项目被删除无法引用下载,会使用vendor来做缓存管理,它是独立的,你可以手动管理引用的包,代码包查找的顺序是向上冒泡\n包同目录下的vendor 包目录向上的最近的一个vendor ... GOPATH src 下的vendor GOROOT src GOPATH src 这样的话, 我们可以把包的依赖都放在 vendor 下,然后提交到仓库,这样可以省却拉取包的时间,并且相对自由,你想怎么改都可以\n1.3.6 最佳实践 # go mod 只是一个依赖包版本管理工具,包的查找顺序还是一样的,使用mod就不用把代码都放到src下来管理,可以根据go.mod文件中记录的版本来索引\n我建议:\n 使用mod管理版本,并使用go vendor来cache依赖包,上传到仓库防止代码包被删除 运行时用到自己的项目,不要使用本地代码,而是保证依赖包都是稳定的,防止忘记提交 如果你想发布包把自己写的模板给别人用,记得提交到仓库 这样就可以单个项目独立下来debug了,依赖包版本也管理上了\nPS: go项目就可以使用mod和vendor,如果要集成其他语言代码为子模块可以使用git submodule\n1.3.7 tips # Q1: 我的包下哪去了?\nA: 依赖的第三方包被下载到了 $GOPATH/pkg/mod 路径下。\nQ2: GO111MODULE 的三个参数 auto 、 on 、 off 有什么区别?\nA: auto 根据是否在 src 下自动判定, on 只用 go.mod , off 只用 src 。\nQ3: 依赖包中的地址失效了怎么办?比如 golang. org/x/… 下的包都无法下载怎么办?\nA: 在 go.mod 文件里用 replace 替换包,例如\nreplace golang.org/x/text =\u0026gt; github.com/golang/text latest 这样, go 会用 github.com/golang/text 替代 golang.org/x/text\nQ4: 在 go mod 模式中,项目自己引用自己中的某些模块怎么办?\nA: go.mod 文件里的第一行会申明 module main ,把这个 main 改为你的项目名,引用的时候就 import \u0026quot;项目名/模块名\u0026quot; 即可。\n 根据官方的说法,从 Go 1.13 开始,模块管理模式将是 Go 语言开发的默认模式。\n 1.3.8 小结 # go mod 是未来的默认模式,未来会取消 go path 也就是src 的方式,但自己的项目目录还是尽量按路径放置,不然回头找不到了\n1.3.9 引用 # Go Modules 掘金\n"},{"id":3,"href":"/1.base/1-4-variables/","title":"1 4 Variables","section":"1.bases","content":"1.4 声明【变量】的各种方式 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.4-variables\n 讲变量就要先知道 go 语言有哪些数据类型。\n1.4.1 数据类型 # 数据类型的出现是为了把数据分成所需内存大小不同的数据。\n 布尔型(bool): 值只可以是常量 true 或者 false。 数字类型: 整型 int 和浮点型 float,支持复数(业务代码用不到),其中位的运算采用补码。 字符串类型(string): 使用UTF-8编码标识Unicode文本。 其他:指针、数组、结构体(struct)、联合体 (union不常用)、函数、切片、接口(interface)、Map 、 Channel 大多数类型都是接触过的,比如c++的结构体,比如python的切片,java的接口,别看类型那么多以后写多了自然就会用了。\ngo 语言声明变量的方式非常简单\n1.4.2 第一种方式、var # var name string 结构为var+变量名+类型\nname = \u0026#34;s\u0026#34; 像这样赋值\n//根据赋值自动判断类型 var p = name 因为name是字符串类型,所以p也是同类型\n//多变量声明,int类型不赋值自动赋值为0,比如d e f var a, b, c = 1, 2, 3 var d, e, f int 一次声明多个类型不同的变量\n//类型不同的多个变量,难看的要死 \tvar ( k int l string ) //这样好看 var m, n, o = \u0026#34;a\u0026#34;, 1, true 1.4.3 方式二、:= # //直接声明并赋值(必须是初次声明才有冒号) p2 := \u0026#34;as\u0026#34; // 多个变量一次性声明并赋值 h, i, j := 1, 2, 3 1.4.4 常量 # 常量就是不可变的变量,定义方式\nconst identifier [type] = value 约定常量全大写表示\nconst A int = 1 const B = 1 const C, D, E = 1, 1, 1 一般常量被用于枚举\nconst ( Success = 0 UnKonw = 1 Error = 2 ) 不过要枚举还是用 go 自带的特殊常量好一点,这种特殊被认为是可以被编译器修改的常量\n//const 出现时被重置为0,每出现一次自动加1 \tconst ( F = iota G = iota H = iota ) F、G、H 值为0,1,2\n当然可以简写成这样,效果是一样的。\nconst ( I = iota J K ) 1.4.5 类型转换 # 没有什么好说的,和其他语言相似,类型转换都是类型+变量的形式,如下。\nvar aInt int = 17 // 一般用这种方式强制转 \tfmt.Printf(\u0026#34;转float64 %f \\n\u0026#34;, float64(aInt)) fmt.Printf(\u0026#34;转string %v \\n\u0026#34;, strconv.Itoa(aInt)) fmt.Printf(\u0026#34;转float64 %f \\n\u0026#34;, float64(aInt))[] 输出\n转float64 17.000000 转string 17 转float64 17.000000 各种类型转字符串\nresString := fmt.Sprintf(\u0026#34;%d %v %v\u0026#34;, 1, \u0026#34;coding3min\u0026#34;, true) fmt.Println(resString) 输出\n1 coding3min true string 和 bytes 的互相转换\n// string to bytes \tresBytes := []byte(\u0026#34;asdfasdf\u0026#34;) // bytes to string \tresString = string(resBytes) fmt.Println(resString) 输出\nasdfasdf 1.4.6 小结 # 本节介绍了常量和变量,以及变量之间简单类型的转换,这里语言的基础,需要熟练掌握,特别是在做算法的时候更是高频用到。\n"},{"id":4,"href":"/1.base/1-5-switch%E5%92%8Ctypeswitch/","title":"1 5 Switch和typeswitch","section":"1.bases","content":"1.5 switch和type switch # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.5-switch-type-switch\n 1.5.1 if else # if 20\u0026gt;0{ fmt.Println(\u0026#34;yes\u0026#34;) } 输出\nyes ifelse\n if 20\u0026lt;0{ }else{ fmt.Println(\u0026#34;no\u0026#34;) } 输出\nno 1.5.2 switch 和 type switch # switch 好理解,是一个替代if else else else接口而提出的,如下,switch 后跟变量,case 后跟常量,只要变量值和常量匹配,就执行该分支下的语句。\nswitch name { case \u0026#34;coding3min\u0026#34;: fmt.Println(\u0026#34;welcome\u0026#34; + name) default: fmt.Println(\u0026#34;403 forbidden:\u0026#34; + name) return } 和c++不同,不需要给每个case都手动加入break,当然switch语句会逐个匹配case语句,一个一个的判断过去,直到有符合的语句存在,执行匹配的语句内容后跳出switch。\nfunc switchDemo(number int) { switch { case number \u0026gt;= 90: fmt.Println(\u0026#34;优秀\u0026#34;) case number \u0026gt;= 80: fmt.Println(\u0026#34;良好\u0026#34;) case number \u0026gt;= 60: fmt.Println(\u0026#34;凑合\u0026#34;) default: fmt.Println(\u0026#34;太搓了\u0026#34;) } } 如果没有一个是匹配的,就执行default后的语句。\n注意switch后可以跟空,如上,原因是之前已经出现过number变量\nswitch { 如果为空,这样case就必须是表达式。\n1.5.3 switch 的高级玩法? # 有一个流传于坊间的神秘玩法,可以用switch语句来判断传入变量的类型,然后做一些羞羞的事情。x是一个未知类型的变量,switch t := x.(type) 用这个方式来赋值,t就是有确定类型的变量。\nswitch t := x.(type) { case int: return t case float64: return int(math.Ceil(t)) } 什么叫未知类型??\n这就是 go 中有意思的地方了, interface{} 类型,是一种神奇的类型,他可以是任何类型的接口,而具体的类型是实现。\nvar x interface{} x = 1 fmt.Println(x) 输出1\n所以完整的函数是这样的\nfunc typeSwitchDemo(x interface{}) int { switch t := x.(type) { case int: return t case float64: return int(math.Ceil(t)) } return 0 } 这个东西有什么用呢??有没有想过如果你有一个场景,你在调用第三方的接口,却发现对方的接口发生了微调,原来的int类型,被转换成了string类型,你必须写出兼容两种方式的代码来解析json。\n那么这个时候,type switch 将会是你的武器。\n感兴趣可以 跑到这里看看,我是怎么使用这个武器的。\nhttps://github.com/golang-minibear2333/golang/blob/master/golang/medium/json_interface/fixed_json.go\n1.5.4 小结 # 通过这一节了解到go语言中无类型语法interface{},这和java种任何类都是集成于一个统一的基类一样\n"},{"id":5,"href":"/1.base/1-6-for-range/","title":"1 6 for Range","section":"1.bases","content":"1.6 循环 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.6-for-range\n 今天 go 语言的内容是循环。\n由于在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。\ngo 语言的循环和其他的没什么不同,只是语法上略微有些差别。\n1.6.1 for 循环方式 1 和 c++、java 相似 # nums := []int{1, 2, 3, 4, 5, 6} for i := 0; i \u0026lt; len(nums); i++ { fmt.Println(i) } 1.6.2 for 循环方式 2 省略赋值和++ # a, b := 1, 5 for a \u0026lt; b { fmt.Println(a) a++ } 1.6.3 for 循环方式 3 迭代 # 优点:不用引入无意义的变量 缺点:不是直接索引,如果数据量极大会有性能损耗 for index, value := range nums { fmt.Printf(\u0026#34;key: %v , value: %v \\n\u0026#34;, index, value) } 当然,你可以把方式 3 中 index 去掉,用_忽略掉key\nfor _, v := range nums { fmt.Printf(\u0026#34;value: %v \\n\u0026#34;, v) } 如果你想忽略掉 value,直接用 key也是可以的,这样就消除了迭代方式的缺点!\nfor i := range nums { fmt.Printf(\u0026#34;value: %v \\n\u0026#34;, nums[i]) } 1.6.4 死循环 # 这样就是一个最简单的死循环,循环条件永远为true也是死循环\nfor { } 1.6.5 break、continue # i := 0 for { fmt.Printf(\u0026#34;死循环测试 %v \\n\u0026#34;, i) i++ if i \u0026gt; 5 { fmt.Println(\u0026#34;满足终止条件,退出\u0026#34;) break //直接跳出循环 \t} if i == 3 { continue //会直接跳过执行后面的语句 \t} fmt.Printf(\u0026#34;死循环测试,第%v次跑到循环结尾\\n\u0026#34;, i) } 输出\n死循环测试 0 死循环测试,第1次跑到循环结尾 死循环测试 1 死循环测试,第2次跑到循环结尾 死循环测试 2 死循环测试 3 死循环测试,第4次跑到循环结尾 死循环测试 4 死循环测试,第5次跑到循环结尾 死循环测试 5 满足终止条件,退出 1.6.6 小结 # 这一节就是全部的循环语法啦\n"},{"id":6,"href":"/1.base/1-7-range%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90/","title":"1 7 Range深度解析","section":"1.bases","content":"1.7 range深度解析 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/1.base/1.7-range-deep\n 1.7.1 range(范围) # range 关键字在 go 语言中是相当常用好用的语法糖,可以用在 for 循环中迭代 array、slice、map、channel、字符串所有涉及到遍历输出的东西。\n1.7.2 怎么用? # 我们在前一节 循环 中初次触及到了 range,也知道他可以省略key,或者省略value来循环遍历的特性,但是这种特性要结合实际情况考量该用哪一个。\n切片迭代\nnums := []int{1, 2, 3} for k, v := range nums { fmt.Printf(\u0026#34;key: %v , value: %v \\n\u0026#34;, k, v) } 这和迭代方式非常适合用for-range语句,如果减少赋值,直接索引num[key]可以减少损耗。如下\nfor k, _:= range nums { map迭代 注意,从 Go1开始,遍历的起始节点就是随机了。\nkvs := map[string]string{ \u0026#34;a\u0026#34;:\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;:\u0026#34;b\u0026#34;, } for k, v := range kvs { fmt.Printf(\u0026#34;key: %v , value: %v \\n\u0026#34;, k, v) } 函数中for-range语句中只获取 key 值,然后跟据 key 值获取 value 值,虽然看似减少了一次赋值,但通过 key 值查找 value 值的性能消耗可能高于赋值消耗。\n所以能否优化取决于 map 所存储数据结构特征、结合实际情况进行。\n字符串迭代(一个一个的输出字符)\nfor k,v := range \u0026#34;hello\u0026#34;{ //注意这里单个字符输出的是ASCII码, //用 %c 代表输出字符 \tfmt.Printf(\u0026#34;key: %v , value: %c \\n\u0026#34;, k, v) } channel (如果不会可以先 mark 下,详细参考后续:go 的并发特性章节)\nch := make(chan int, 10) ch \u0026lt;- 11 ch \u0026lt;- 12 close(ch) // 不用的时候记得关掉,不关掉又没有另一个goroutine存在会死锁哦,可以注释掉这一句体验死锁 for x := range ch { fmt.Println(x) } 结构体\ntmp := []struct{ int string }{ {1, \u0026#34;a\u0026#34;}, {2, \u0026#34;b\u0026#34;}, } for k,v := range tmp{ fmt.Printf(\u0026#34;k:%v, v:%v \\n\u0026#34;,k,v) } 注意:由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的。\n1.7.3 有可能会遇到的坑! # 由于range遍历时value是值的拷贝,如果这个时候遍历上一节声明的结构体时,修改value,原结构体不会发生任何变化!\nfor _,v := range tmp{ v.a = 2 } 两次输出一致\nk:0, v:{1 a} k:1, v:{2 b} k:0, v:{1 a} k:1, v:{2 b} 1.7.4 编程 Tips # 遍历过程中可以适情况放弃接收 index 或 value,可以一定程度上提升性能 遍历 channel 时,如果 channel 中没有数据,可能会阻塞 尽量避免遍历过程中修改原数据 1.7.5 小结 # range可以用于for 循环中迭代 array、slice、map、channel、字符串所有涉及到遍历输出的东西。 for-range 的实现实际上是C风格的for循环 使用index,value接收range返回值会发生一次数据拷贝 "},{"id":7,"href":"/2.func-containers/2-1-func/","title":"2 1 Func","section":"2.func-containers","content":"2.1 函数简单使用和基本知识解析 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.1-func\n 这里的源码有多个,本节相关的有\n# 函数的简单使用 main.go # 函数当作变量使用,当做 参数传递 function_value.go # 值传递和引用传递 more.go # 递归函数 recursive.go 拓展代码有\n# 函数当作变量使用,当做 参数传递的分页实践 function_value_good_demo.go # 函数方法(go中定义一个类) go_class.go 2.1.1 基本原理 # 函数,几乎是每种编程语言的必备语法,通过函数把一系列的动作汇总起来,在不同的地方重复使用。\n我们在数学中曾经就使用过函数,他的形式类似于y=f(x),这就是一个完整的调用过程,y就是函数计算后得到的值,x就是传入的变量。\n2.1.2 怎么用? # 相信在看这个教程的人肯定已经接触过其他的编程语言,我就不多废话了,就是干。\n go语言中最基本的函数是这样的,以func为关键字标记函数\nfunc functionParam(num int) { } 当然了,可以有多个形参,类型相同时可以省略,如下\n//多个参数的函数 func functionParams(a, b int, c string) { } 上面说过的函数都没有返回值,一般的函数都有返回值,没有返回值的函数要么是引用传递,可以直接改变参数内容,要么就是用于单元测试或者打印输出等。\n 没有返回值的函数就像一个不完整的男人,只能接受不能输出,来看看这个男人有一个输出的情况。\n返回值标记在函数第一个括号后面,由于go语言是强类型语言,但又和python不同,要写出返回值类型。\n//一个返回值 func funcReturnOne() int { return 1 } 如果说是有多个返回值,要用打括号括起来。\n//多个返回值 func funReturnMany() (int, int) { return 1, 2 } 上面的返回值全部都是匿名的,可以赐他一个名字,函数中不用定义返回值,可以省略几行代码。\n//返回值有名称 func funReturnName() (res int) { //var res int 省掉了 \tres = 1 + 1 return } 用返回就有接收,函数外部用这种方式接收\n//接收多个返回值 \ta, b := funReturnMany() 2.1.3 值传递,引用传递 # 刚刚有说到函数没有返回值的时候,要么是只需要打印结果,要么是只做单元测试,除了这两种情况,没有返回值的函数就是做了很多事情的你没有和老板汇报一样,没有任何意义!\n引用传递和c++类似,先举个值传递的例子。\n//值传递 func noChange(a, b int) { tmp := a a = b b = tmp } 调用打印结果看看\na, b := 1, 2 fmt.Printf(\u0026#34;原值 a:%v,b:%v \\n\u0026#34;, a, b) noChange(a, b) //值传递,并没有修改原值 \tfmt.Printf(\u0026#34;值传递后 a:%v,b:%v \\n\u0026#34;, a, b) 看!像不像任劳任怨的你,忙活半天被老板喜欢的小张抢了功劳。\n原值 a:1,b:2 值传递后 a:1,b:2 下面来看看引用传递的例子。在类型前加一个星号代表该参数是一个指针\n// 引用传递,参数加*号代表指针 func change(a,b *int){ tmp := *a *a = *b *b = tmp } 学过c++再来学go简直是如虎添翼,c++中有一个指针的概念go语言里也有。\n//引用传递,\u0026amp;就是c中的取地址 \tchange(\u0026amp;a,\u0026amp;b) fmt.Printf(\u0026#34;引用传递后 a:%v,b:%v \\n\u0026#34;, a, b) 输出结果,可以看到值被调换了。引用传递需要加\u0026amp;符号,术语叫取地址。函数里的对他做的任何操作都会改变原来的变量内容。\n引用传递后 a:2,b:1 上面的例子传入的是指针,还有一种叫引用类型,和指针的区别是不需要星号和\u0026amp;,对他的修改会直接改动到原有变量的值。\nps:go语言中只有三种引用类型,slice(切片)、map(字典)、channel(管道)\n2.1.4 函数进阶 # 上面说的东西都很简单了,基本学过任何一门语言的人都能瞬间看懂,和python、c++、javascript一样,go中也有把函数当作参数传递的语法。\n像这样,functionValue函数的形参里有一个名为do的函数,需要提前指定do函数有什么参数和返回值。\nfunc functionValue(a, b int, do func(int, int) int) { fmt.Println(do(a, b)) } 然后do(a,b)是在functionValue内部调用的。这种特性有什么用呢?定义两个参数为int,返回为int的函数。\nfunc add(a, b int) int { return a + b } func sub(a, b int) int { return a - b } 因为规则符合do函数的规则,两个都可以传递过去,看!这就不用修改函数内部而出现了两种效果。\nfunctionValue(1, 1, add) functionValue(1, 1, sub) 在设计模式里,这种方式叫装饰器模式(Decorator Pattern):允许向一个现有的对象添加新的功能,同时又不改变其结构。\n 当然,你也不必每次传递函数的时候都憨厚老实的定义一个新函数,因为有时候你定义的函数就只会在这里用到,只不过是把实现放在调用外部,而不修改原函数代码罢了。\n//匿名函数 \tfunctionValue(1, 1, func(i1 int, i2 int) int { return i1 * i2 }) 上面这个例子多看几遍啊!!\n2.1.5 实际的使用 # 你可以参考函数测速例子\n 源码位置:https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.1-func/append_string.go\n 定义一个测速函数。\nfunc speedTime(handler func() (string) { t := time.Now() handler() elapsed := time.Since(t) // 利用反射获得函数名 funcName := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() fmt.Println(funcName+\u0026#34;spend time:\u0026#34;, elapsed) } 传入不同的函数都可以测速度。\nspeedTime(appendStr) speedTime(appendStrQuick) 小Tips:\n 还有你可以传filter函数做过滤,mapping做映射等实际的用法 有时候也可以作为排序递增,递减的依据 2.1.6 小结 # 本节讲述了Go中函数的基本语法,包括定义、多值返回,函数的值传递和引用传递,还可以当变量来用,可以把函数当参数来传递\n"},{"id":8,"href":"/2.func-containers/2-2-%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0%E5%92%8C%E9%97%AD%E5%8C%85/","title":"2 2 匿名函数和闭包","section":"2.func-containers","content":"2.2 匿名函数和闭包 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.2-no-name-func\n 与本节相关的有,匿名函数没有留源代码\n2.2.1 匿名函数 # 话不多说,今天小熊就带各位家人感受下go语言函数中的高级语法。\n在 前面的文章 里我们学会了把函数当作变量传递,可以在不改动原有函数内部实现的情况下,改变函数实现细节(设计模式:装饰器)。\n这种情况下的作为变量传递的函数往往只有这一个地方用到了,其他地方不会重复使用。那就没必要单独定义一个函数在外面!(多此一举的事本熊不做!)\nlike this:\nfunc functionValue(a, b int, do func(int, int) int) { fmt.Println(do(a, b)) } //使用匿名函数的方法调用他 实现匿名加函数 functionValue(1,2,func(a,b int) int{ return a+b }) //使用匿名函数的方法调用他 实现匿名减函数 functionValue(1,2,func(a,b int) int{ return a-b }) 输出\n3 -1 在调用的时候我们才实现了一个匿名函数(没有名字的函数)\n那是不是只有把函数当变量传递的时候才用到匿名函数呢?并,不,是!\n各位同学,让我上黑板给大家实现一个简单的匿名函数用法。\nf := func(i int) { fmt.Println(i) } f(1) 把匿名函数赋值给一个变量(这里是f),f就是他的函数名,后面就可以直接调用啦~,但是这种简单使用的情况实际上会不会用到呢?很残酷,几乎没有。\n匿名函数配合下面的场景使用效果更佳。\n2.2.2 闭包 # 你有没有一种情况,常常要定义好多全局变量来共享数据,这种变量一旦多了非常难看,还会污染环境,有没有一种办法,可以通过重复调用同一个函数,来修改函数内部的变量呢?\n我翻来覆去发现是真的有!这个东西就叫闭包!\n 闭包的简单实现,把函数定义在函数内部,并当作返回值返回。\nfunc closureSample() func() { count := 0 return func() { count ++ fmt.Printf(\u0026#34;调用次数 %v \\n\u0026#34;, count) } } 怎么用才爽?我先丧心病狂的调用两次closureSample函数,得到两个函数c1、c2,这两个函数就是closureSample函数的返回值,类型是一个匿名函数。\nc1, c2 := closureSample(), closureSample() 疯狂调用!!!\n c1() c1() c1() // 你会发现c2又从1开始输出,因为两个函数的变量是独立使用的 \tc2() c2() 输出\n调用次数 1 调用次数 2 调用次数 3 调用次数 1 调用次数 2 调用次数 3 神奇不神奇!在调用c2的时候,完全没有影响到c1!\n这是因为各个函数是独立使用一套自己的内部变量,互相不影响,所以闭包也可以当测试用例使用。\n用来传入不同的实现,重复调用得到不同的返回,不用定义全局变量。\n 好处:可以减少全局变量防止变量污染 坏处:延长了局部变量和函数的生命周期,增加了 gc 的压力 2.2.3 闭包形式 2 # 通过上面的例子,不难发现闭包内部的匿名函数可以使用到外部的变量。\n闭包形式 2,立即执行函数,声明完以后加括号,用以表示即刻调用。\nfunc() { // to do something \t}() 2.2.4 闭包存在的 bug # go 里创建一个协程(类似于子线程)非常的容易,只要在语句前加一个go关键字就可以了。看看下面这个函数会出现什么问题。\nfor i := 0; i \u0026lt; 3; i++ { fmt.Printf(\u0026#34;第一次 i 产生变化中 %v \\n\u0026#34;, i) go func() { fmt.Printf(\u0026#34;第一次输出: %v\\n\u0026#34;, i) }() } time.Sleep(time.Second) 协程创建完以后立即会执行,但是协程创建这个事件和协程执行代码是分离的,他可以全部创建完再执行,而且主线程和协程是同时运行的(并发),有可能主线程执行完了,协程还没执行。\n这个时候协程才会调用外部的变量,i 已经变成 3 了。\n第一次 i 产生变化中 0 第一次 i 产生变化中 1 第一次 i 产生变化中 2 第一次输出: 3 第一次输出: 3 第一次输出: 3 解决办法,创建副本,可以给匿名函数加一个参数,传值过来自动生成副本\nfor i := 0; i \u0026lt; 3; i++ { fmt.Printf(\u0026#34;第二次 i 产生变化中 %v \\n\u0026#34;, i) go func(tmp int) { fmt.Printf(\u0026#34;第二次输出: %v\\n\u0026#34;, tmp) }(i) } time.Sleep(time.Second) 输出\n第二次 i 产生变化中 0 第二次 i 产生变化中 1 第二次输出: 0 第二次 i 产生变化中 2 第二次输出: 2 第二次输出: 1 第二种创建副本的形式\nfor i := 0; i \u0026lt; 3; i++ { fmt.Printf(\u0026#34;第三次 i 产生变化中 %v \\n\u0026#34;, i) tmp := i go func() { fmt.Printf(\u0026#34;第三次输出: %v\\n\u0026#34;, tmp) }() } time.Sleep(time.Second) 输出\n第三次 i 产生变化中 0 第三次 i 产生变化中 1 第三次 i 产生变化中 2 第三次输出: 0 第三次输出: 2 第三次输出: 1 2.2.5 小结 # 匿名函数在做参数传递时常用于设计模式中的订阅模式和策略模式、装饰器模式、调用链模式,同时匿名函数可以访问到外部变量的特性,也常常用于并发,在用于并发时要小心闭包bug。\n"},{"id":9,"href":"/2.func-containers/2-3-%E5%8F%AF%E5%8F%98%E5%8F%82%E6%95%B0/","title":"2 3 可变参数","section":"2.func-containers","content":"2.3 可变参数 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.3-varargs\n 接连两篇函数专题深度解析,相信大家已经对函数的语法有了深入的了解。\n这次小熊给大家带来了一个函数的特性【可变参数】,作为函数专题的结束。\n2.3.1 有没有发现? # 我们有时候会用到的输出、错误输出、字符串格式化系统函数,你可以传入任意个数的参数,他全都能处理!\nfmt.Println(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;,\u0026#34;d\u0026#34;) 就像一个任劳任怨的老父亲,那到底是为什么呢?\n2.3.2 因为 # 因为在go语言中语言级别自带了一种语法,可以声明可变参数!\nfunc 函数名(固定参数,v ...T) (返回参数列表){ 函数体 } 2.3.3 怎么用? # 先和小熊一起试试,写一个不定参数累加的函数\nfunc sum(t ...int) (res int) { for _, v := range t { res += v } return res } fmt.Println(sum(1, 2, 3, 4, 5)) 输出结果\n15 2.3.4 如果连参数类型都不知道,怎么办? # 上一节我们已知参数类型是int,还记得我们前面说过的switch判断类型做处理的例子吗? switch和type switch。\n参考这个例子重写下函数,让他可以接收任意类型的参数。\nfunc sumNum(t ...interface{}) (res float64){ for _,tmp := range t{ switch v :=tmp.(type) { case int: res += float64(v) case float64: res+= v case float32: res += float64(v) } } return res } 测试下\nfmt.Println(sumNum(1,2.1,\u0026#34;asd\u0026#34;,true)) 因为忽略了输出\n3.1 但是上面的例子并没有覆盖全部的数字,如果一个一个类型的匹配会疯掉的。有没有更好的方法,可以一下子匹配到所有的数字?\nfunc sumNum(t ...interface{}) (res float64) { for _, tmp := range t { switch v := tmp.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128: convertStr := fmt.Sprintf(\u0026#34;%v\u0026#34;, v) convertFloat64, _ := strconv.ParseFloat(convertStr, 64) res += convertFloat64 } } return res } 上面的代码在case里一下子匹配了所有可能的数字类型,再用格式化输出转换成字符串,最后转换成float64来使用,这是一种折中的办法,速度可能会比较慢(因为格式化内部逻辑比较复杂消耗速度)。\n为了保证速度还是不要省掉一大堆的case,明确了类型再做强转。\n2.3.5 留给你的寻找的答案 # 有没有一种只留一个case,同时处理速度又快的方法呢?\n—— 爱你们的小熊\n2.3.6 小结 # 本节介绍了不定参数,知道参数类型与不知道参数类型的处理方法,你可以把这种模式用于类型转换、以及策略模式。\n在fmt包中,有很多Print就是使用了不定参数,有兴趣可以看一下源码。\n"},{"id":10,"href":"/2.func-containers/2-4-map/","title":"2 4 Map","section":"2.func-containers","content":"2.4 map # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.4-map\n 2.4.1 映射关系容器 map # Go语言提供的映射关系容器为 map , map 使用散列表hash实现。查找复杂度为O(1),和数组一样,最坏的情况下为O(n),n为元素总数。\n这就是Go中map的定义格式。\nmap[keyType] valueType 注意了,map 是一种引用类型,初值是nil,定义时必须用make来创建,否则会报错\npanic: assignment to entry in nil map 必须要申请空间,所有的引用类型都要这么做\nvar m map[string]string m = make(map[string]string) 当然,也可以这么写\nm := make(map[string]string) 2.4.2 使用 # 赋值\nm[\u0026#34;name\u0026#34;] = \u0026#34;coding3min\u0026#34; m[\u0026#34;sex\u0026#34;] = \u0026#34;man\u0026#34; 循环遍历\nfor key := range m { // 原来不用Printf也可以完成拼接输出啊! \tfmt.Println(\u0026#34;key:\u0026#34;, key, \u0026#34;,value:\u0026#34;, m[key]) } 删除集合元素\ndelete(m, \u0026#34;name\u0026#34;) PS: 在取值的时候m[key],假如key不存在,不会报错,会返回value类型的默认值,比如int类型默认值为0\n当然了,如果你想明确的知道元素是否存在,如下:\nif value, ok := m[key]; ok { fmt.Println(key, \u0026#34;存在,值为:\u0026#34;, value) } else { fmt.Println(key, \u0026#34; 不存在\u0026#34;) } 2.4.3 map 内部元素的修改 # map 可以拷贝吗?\nmap 其实是不能拷贝的,如果想要拷贝一个 map ,只有一种办法就是循环赋值,就像这样\noriginalMap := make(map[string]int) originalMap[\u0026#34;one\u0026#34;] = 1 originalMap[\u0026#34;two\u0026#34;] = 2 // Create the target map targetMap := make(map[string]int) // Copy from the original map to the target map for key, value := range originalMap { targetMap[key] = value } 如果 map 中有指针,还要考虑深拷贝的过程\noriginalMap := make(map[string]*int) var num int = 1 originalMap[\u0026#34;one\u0026#34;] = \u0026amp;num // Create the target map targetMap := make(map[string]*int) // Copy from the original map to the target map for key, value := range originalMap { var tmpNum int = *value targetMap[key] = \u0026amp;tmpNum } 如果想要更新 map 中的value,可以通过赋值来进行操作\nmap[\u0026#34;one\u0026#34;] = 1 但如果 value 是一个结构体,可以直接替换结构体,但无法更新结构体内部的值\noriginalMap := make(map[string]Person) originalMap[\u0026#34;minibear2333\u0026#34;] = Person{age: 26} originalMap[\u0026#34;minibear2333\u0026#34;].age = 5 你可以 试下源码函数 updateMapValue ,会报这个错误\n Cannot assign to originalMap[\u0026ldquo;minibear2333\u0026rdquo;].age\n 问题链接 issue-3117 , 其中 ianlancetaylor 的回答很好的解释了这一点\n简单来说就是map不是一个并发安全的结构,所以,并不能修改他在结构体中的值。\n这如果目前的形式不能修改的话,就面临两种选择,\n 1.修改原来的设计; 2.想办法让map中的成员变量可以修改, 因为懒得该这个结构体,就选择了方法2\n要么创建个临时变量,做拷贝,像这样\ntmp := m[\u0026#34;foo\u0026#34;] tmp.x = 4 m[\u0026#34;foo\u0026#34;] = tmp 要么直接用指针,比较方便\noriginalPointMap := make(map[string]*Person) originalPointMap[\u0026#34;minibear2333\u0026#34;] = \u0026amp;Person{age: 26} originalPointMap[\u0026#34;minibear2333\u0026#34;].age = 5 2.4.4 能够在并发环境中使用的map # Go中的map在并发读的时候没问题,但是并发写就不行了(线程不安全),会发生竞态问题。\n所以有一个叫sync.Map的封装数据结构供大家使用,简单用法如下: 定义和存储\nvar scene sync.Map scene.Store(\u0026#34;name\u0026#34;, \u0026#34;coding3min\u0026#34;) scene.Store(\u0026#34;age\u0026#34;, 11) 取值\nv, ok := scene.Load(\u0026#34;name\u0026#34;) if ok { fmt.Println(v) } v, ok = scene.Load(\u0026#34;age\u0026#34;) if ok { fmt.Println(v) } 输出\ncoding3min 11 删除和遍历,这里遍历就用到了 函数当作参数传递 和 匿名函数 的知识。\nscene.Delete(\u0026#34;age\u0026#34;) scene.Range(func(key, value interface{}) bool { fmt.Println(\u0026#34;key:\u0026#34;,key,\u0026#34;,value:\u0026#34;,value) return true }) 2.4.5 小结 # 本节介绍了字典map类型,这种类型在很多语言中都有,并且学习了它的增加删除元素的方法,以及更新value要注意的点。\n还介绍了并发环境下使用的线程安全的 sync.Map。\n"},{"id":11,"href":"/2.func-containers/2-5-%E6%95%B0%E7%BB%84%E5%92%8C%E5%88%87%E7%89%87/","title":"2 5 数组和切片","section":"2.func-containers","content":"2.5 数组和切片 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.5-arrray https://github.com/golang-minibear2333/golang/blob/master/2.func-containers/2.5-slice\n 2.5.1 Golang中的数组 # 其实在 循环 那一节用到过数组,我快速介绍一下。\n 数组中是固定长度的连续空间(内存区域) 数组中所有元素的类型是一样的 var a1 [10]int //初始化数组 \tvar b1 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} 多维数组\n//声明二维数组,只要 任意加中括号,可以声明更多维,相应占用空间指数上指 \tvar arr [3][3]int //赋值 \tarr = [3][3]int{ {1, 2, 3}, {2, 3, 4}, {3, 4, 5}, } 2.5.2 何谓切片? # 类比c语言,一个int型数组int a[10],a的类型是int*,也就是整型指针,而c语言中可以使用malloc()动态的分配一段内存区域,c++中可以用new()函数。例如:\nint* a = (int *)malloc(10); int* b = new int(4); 此时,a和b的类型也是int*,a和b此时分配内存的方式类似于go语言中的切片。\nGo的数组和切片都是从c语言中延续过来的设计。\n2.5.3 有何不同? # var sliceTmp []int 可以看到和c不同的是,go可以声明一个空切片(默认值为nil),然后再增加值的过程中动态的改变切片值大小。\n2.5.4 怎么动态增加? # 增加的方式只有一种,使用append函数追加。\nsliceTmp = append(sliceTmp, 4) sliceTmp = append(sliceTmp, 5) 每个切片有长度len和容量cap两个概念,长度是我们最熟知的,和数组长度相同,可以直接用来遍历。\nfor _,v := range slice1{ fmt.Println(v) } 用切糕来对比\n 每个切片,在声明或扩建时会分配一段连续的空间,称为容量cap,是不可见的;真正在使用的只有一部分连续的空间,称为长度len,是可见的。\n每次append时,如果发现cap已经不足以给len使用,就会重新分配原cap两倍的容量,把原切片里已有内容全部迁移过去。\n新分配的空间也是连续的,不过不一定直接在原切片内存地址处扩容,也有可能是新的内存地址。\n2.5.5 切片的长度与容量,len cap append copy # slice1 := []int{1, 2, 3} 普通切片的声明方式,长度和容量是一致的。\nlen=3 cap=3 slice=[1 2 3] 当然,控制权在我们手上,我们可以自己控制长度和容量,\nslice1 = make([]int, 3, 5) // 3 是长度 5 是容量 输出\nlen=3 cap=5 slice=[0 0 0] 尝试使用一般的方式扩容\nslice1[len(slice1)] = 4 //报错 panic: runtime error: //index out of range [3] with length 3 这种方式是会报错的,虽然容量是 5 ,但是数组长度是3,这里是以长度为准,而不是容量,append内部如果超过容量相当于创建了一个新数组,每个新数组都是定长的,只不过外部是切片。\n尝试扩容\nslice1 = append(slice1, 4) 输出,可以发现len扩容了!\nlen=4 cap=5 slice=[0 0 0 4] 让我们连续扩容,让容量超过5\nslice1 = append(slice1, 5) slice1 = append(slice1, 6) // 到这里长度超过了容量,容量自动翻倍为 5*2 输出\nlen=6 cap=10 slice=[0 0 0 4 5 6] 上面的过程,我 用自己的代码模拟一遍\n// 上面容量自动翻倍的过程可以看作和下面一致 \tslice1 = make([]int, 3, 5) // 3 是长度 5 是容量 \tslice1 = append(slice1, 4) slice1 = append(slice1, 5) // 长度不变,容量自动翻倍为 5*2 \tslice2 := make([]int, len(slice1), (cap(slice1))*2) // 拷贝 slice1 的内容到 slice2 // 注意是后面的拷贝给前面 \tcopy(slice2, slice1) slice2 = append(slice2, 6) 你理解容量,长度的概念了吗?\n2.5.6 切片的复制 # 切片的复制,回顾一下,我们原来是用copy函数\nslice2 := make([]int, len(slice1), cap(slice1)) /* 拷贝 slice1 的内容到 slice2 */ copy(slice2, slice1) // 注意是后面的拷贝给前面 切片还有一种方式复制方式,比较快速\nslice3 := slice2[:] 但是有一种致命的缺点,这是浅拷贝,slice3和slice2是同一个切片,无论改动哪个,另一个都会产生变化。\n可能这么说你还是不能加深理解。在源码 bytes.buffer 中出现了这一段\nfunc (b *Buffer) Bytes() []byte { return b.buf[b.off:] } 我们在读入读出输入流的时候,极易出现这样的问题\n下面的例子,使用abc模拟读入内容,修改返回值内容\nbuffer := bytes.NewBuffer(make([]byte, 0, 100)) buffer.Write([]byte(\u0026#34;abc\u0026#34;)) resBytes := buffer.Bytes() fmt.Printf(\u0026#34;%s \\n\u0026#34;, resBytes) resBytes[0] = \u0026#39;d\u0026#39; fmt.Printf(\u0026#34;%s \\n\u0026#34;, resBytes) fmt.Printf(\u0026#34;%s \\n\u0026#34;, buffer.Bytes()) 输出,可以看出会影响到原切片内容\nabc dbc dbc 这种情况在并发使用的时候尤为危险,特别是流式读写的时候容易出现上一次没处理完成,下一次的数据覆盖写入的错乱情况\n2.5.7 截取部分元素 # 切片之所以为切片,就是可以把部分元素截取出来\nslice2的值是[0 0 0 4 5 6],现在有一个需求,要截取第2个元素出来\nslice3 := slice2[0:1] 输出\nlen=1 cap=10 slice=[0] 我们分别修改slice3和slice2\nslice3[0] = 1 slice2[0] = 2 printSlice(slice2) printSlice(slice3) 发现输出\nlen=6 cap=10 slice=[2 0 0 4 5 6] len=1 cap=10 slice=[2] 说明,截取出现的元素依然是同一块内存(切片是引用类型的)。\n所以截取部分元素之后,还是得用copy来复制一遍,如下。\nslice2 = []int{0, 0, 0, 1, 2, 3} slice3 = make([]int, 1, 1) copy(slice3, slice2[0:1]) 2.5.8 工具函数补充 # 排序工具函数\nslice2 = []int{0, 3, 0, 1, 2, 0} sort.Ints(slice2) fmt.Println(slice2) 输出\n[0 0 0 1 2 3] 其他知识参考 排序用户自定义数据集\n2.5.9 小结 # 本节介绍了切片与数组的区别,动态增加,容量和长度的概念,以及len cap append copy 函数的使用,还介绍了切片的复制和截取。\n"},{"id":12,"href":"/3.grammar-advancement/3-1-point/","title":"3 1 Point","section":"3.grammar-advancements","content":"3.1 指针讨论 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/3.grammar-advancement/3.1-point\n 3.1.1 指针 # c 中有指针的概念,在 go 中也有,但是实际上用的比较少,因为指针容易出错,而且不易阅读。\n每个变量都有他的地址\nvar a int fmt.Printf(\u0026#34;a 的地址是:%p \\n\u0026#34;, \u0026amp;a) 输出\na 的地址是:0xc0000b2008 指针用来存地址\n//声明 变量名 + 指针类型 , 命令规则以ptr结尾 var ptr *int /* 指向整型*/ // var fp *float32 /* 指向浮点型 */ ptr = \u0026amp;a // 变量内部存的值是普通类型,指针内部存的值是地址 fmt.Printf(\u0026#34;ptr 存的值是:%p \\n\u0026#34;, ptr) 输出,可以看到 ptr 存的值就是 a 的地址。\nptr 存的值是:0xc0000b2008 存的就是 a 的地址,ptr 的指向*ptr 肯定就是 a 本身了。\nif a == *ptr { fmt.Println(\u0026#34;a == *ptr\u0026#34;) } 输出\na == *ptr 3.1.2 指针的作用 # 指针可以消灭掉返回值,直接对参数做改变。\n定义一个交换函数,形参为指针类型\nfunc swap(x *int, y *int) { var temp int temp = *x /* 保存 x 地址的值 */ *x = *y /* 将 y 赋值给 x */ *y = temp /* 将 temp 赋值给 y */ } 调用\na := 100 b := 200 //操作地址,不需要返回 swap(\u0026amp;a, \u0026amp;b) fmt.Printf(\u0026#34;交换后 a 的值 : %d\\n\u0026#34;, a) fmt.Printf(\u0026#34;交换后 b 的值 : %d\\n\u0026#34;, b) 输出\n交换后 a 的值 : 200 交换后 b 的值 : 100 虽然可以这么做,但是不推荐,因为 go 比 c++ 多出来多返回值的特性,所以这里写在返回里可读性更强。\nPS1: 但如果你的参数是比较复杂的类型,比如数组。用指针可以节省空间。\nPS2: 对引用类型的操作会改变原引用类型的值,这里与指针有异曲同工之妙。\n3.1.3 多维指针 # 刚刚用到的指针,只不过指向一个变量的地址,他就被叫做一维指针。\nvar ptr *int ptr = \u0026amp;a 指针本身也是一个变量,是变量就有地址,所以指针也可以被取地址。\nvar ptr *int pptr = \u0026amp;ptr *int 类型的指针存的是 int 类型数据的地址,得到 *变量类型 就是他的指针,推导出指向 *int 变量的指针为 **int 类型,这种类型被称为二维指针,每多一个 * 就多一个维。\nvar a int var ptr *int //一维 var pptr **int // 二维 var ppptr ***int // 三维 ptr = \u0026amp;a pptr = \u0026amp;ptr ppptr = \u0026amp;pptr fmt.Printf(\u0026#34;a的地址:%p \\n\u0026#34;, \u0026amp;a) fmt.Printf(\u0026#34;ptr存的地址:%p \\n\u0026#34;, ptr) fmt.Printf(\u0026#34;pptr存的地址的指向:%p \\n\u0026#34;, *pptr) fmt.Printf(\u0026#34;ppptr存的地址的指向的指向:%p \\n\u0026#34;, **ppptr) 输出\na的地址:0xc000014090 ptr存的地址:0xc000014090 pptr存的地址的指向:0xc000014090 ppptr存的地址的指向的指向:0xc000014090 PS1: 日常工作中,不建议使用多维指针,可读性不好,容易犯错误,一层指针能搞定的,一定不要使用多维炫技术。不然过几个月你自己都看不懂。\nPS2: 不得不使用二维指针的场景:你希望在一个函数的参数中改变一个指针的值,你就只能传这个指针的指针给这个函数。\nPS3:多维指针的唯一好处:减少传参\n你在工作中啥时候用到了指针/多维指针?\n3.1.4 小结 # 在Java中没有指针的概念,但是有引用的概念,在C++中比较常见,我们操作内存一定会用到指针,存储了变量的地址。\n为了程序的可读性,一般只会用到一维指针,掌握指针的概念,后面还有大用。\n"},{"id":13,"href":"/3.grammar-advancement/3-2-struct/","title":"3 2 Struct","section":"3.grammar-advancements","content":"3.2 结构体 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/3.grammar-advancement/3.2-struct\n 3.2.1 go 语言中的结构体 # 和 c++ 的结构体类似,如下定义一个结构体类型。\ntype Body struct { name string age int } 像这样就可以使用\nvar body Body body.name = \u0026#34;coding3min\u0026#34; body.age = 12 fmt.Println(body) 输出\n{coding3min 12} 3.2.2 go 中的类 # 结构体在 go 中是最常用的一种语法,有没有想过为什么?\n这是因为我们学过一些面向对象的语言,其中有一个叫类的概念,但是 go 里面没有。\ngo 用一种特殊的方式,把结构体本身看作一个类。\n一个成熟的类,具备成员变量和成员函数,结构体本身就有成员变量,再给他绑定上成员函数,是不是就可以了!\ntype people struct { name string } func (p people) toString() { fmt.Println(p.name) fmt.Printf(\u0026#34;p的地址 %p \\n\u0026#34;, \u0026amp;p) } 上面给 people 结构体绑定了一个函数, 调用下看看\np1 := people{\u0026#34;coding3min\u0026#34;} p1.toString() 按照 toString() 方法的内容,先输出 name 再输出 p的地址\ncoding3min p的地址 0xc0001021f0 #这里的地址一会有用 再绑定一个函数,你想想和上面的函数有什么区别,注意 60% 的人第一眼都没看出来\nfunc (p *people) sayHello() { fmt.Printf(\u0026#34;Hello! %v \\n\u0026#34;, p.name) fmt.Printf(\u0026#34;*p的地址 %p \\n\u0026#34;, p) } 可以注意到,和 toString() 函数不同的是, sayHello() 用了指针的方式进行绑定。\n输出,可以注意到这里的地址和上面的不同。\nHello! coding3min *p的地址 0xc00008e1e0 这两种绑定方式,都是相当于给结构体绑定了函数,这个结构体等价于对象,唯一的不同点就是如果使用 * 绑定函数,那么这种对象就是单例的,引用的是同一个结构体。\np1 := people{\u0026#34;coding3min\u0026#34;} p1.sayHello() p2 := \u0026amp;people{\u0026#34;tom\u0026#34;} p2.sayHello() 输出,可以看到地址一致。\n*p的地址 0xc00008e220 p2的地址 0xc00008e220 3.2.3 一些拓展的结构体知识 # 声明时赋值\nbody2 := Body{ \u0026#34;tom\u0026#34;, 13, } 结构体数组\nbodys := []Body{ Body{\u0026#34;jack\u0026#34;, 12}, Body{\u0026#34;lynn\u0026#34;, 18}, } 匿名结构体,一般用来存测试用例\nclass1 := struct { bodys []Body }{ []Body{Body{\u0026#34;jerry\u0026#34;, 24}}, } 3.2.4 小结 # 通过这篇文章,你应该对 go 语言中的 对象 有一个直观的体验。\n 学会如何给结构体绑定方法 了解绑定方法时是否加 * 号(指针)的区别 学会声明时赋值、结构体数组、匿名结构体的知识 我们在 java 里学习过 interface (接口),通过接口定义一系列的函数(标准),实现接口的对象需要实现所有的方法,那 go 语言中是否有这种语法呢?我们下次再见!\n"},{"id":14,"href":"/3.grammar-advancement/3-3-%E6%8E%A5%E5%8F%A3%E4%B8%8E%E5%A4%9A%E6%80%81/","title":"3 3 接口与多态","section":"3.grammar-advancements","content":"3.3 接口与多态 # 今天和大家聊聊 golang 的接口( interface )\n 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/3.grammar-advancement/3.3-interface\n 3.3.1 接口 # 接口同 java 一样,可以把一堆有共性的方法定义在里面,但是比 java 灵活的是,不需要显式实现接口,你可以自己控制实现哪些方法。\n不需要显式实现的意思是,不需要像 java 那样 implements interface 写出来,别急,看完下面的例子就知道了。\n定义一个接口。\ntype humanInterface interface { eat() string play() string } 定义一个结构体(类)\ntype man struct { name string } 实现接口,语法和 给结构体添加方法 一样,完全看不出来 接口 的身影。\nfunc (p man) eat() string { return \u0026#34;eat banana\u0026#34; } func (p man) play() string { return \u0026#34;play game\u0026#34; } 上面的代码给结构体添加了和接口一样的方法,只要完全实现接口中的方式,默认这就实现接口(隐式)。\n用下面这样的格式,把结构体赋值给接口来实现他 接口实例 = new(类型)\nvar human humanInterface human = new(man) fmt.Println(human.eat()) fmt.Println(human.play()) 输出\neat banana play game PS: new 关键字和 c++ 中的不同,释放内存由 go 的垃圾处理机来做,不需要自己释放内存。\n3.3.2 这不是接口 # 上面的是一个很简单实现接口的例子。 要注意的是,必须实现了所有接口的方法才算是实现了这个接口。\n假如我们只实现了接口中的一个方法,会发生什么事?\ntype dogInterface interface { eat() string play() string } type dog1 struct { name string } func (d dog1) eat() string { return \u0026#34;Eat dog food\u0026#34; } var dog dogInterface dog = new(dog1) 报错\n报错:Cannot use 'new(dog1)' (type *dog1) as type dogInterface in assignment Type does not implement 'dogInterface' as some methods are missing: play() string more... 3.3.3 多态 # 当然,多态是面向对象的灵魂, go 怎么能没有?\n这是一个以接口为参数的函数,方法内调用了接口中方法。\nfunc humanDoWhat(p humanInterface) { fmt.Println(p.eat()) fmt.Println(p.play()) } 传入不同的类(结构体)\nw := woman{\u0026#34;lisa\u0026#34;} m := man{\u0026#34;coding3min\u0026#34;} // 多态的含义就是不需要修改函数,只需要修改外部实现 // 同一个接口有不同的表现 humanDoWhat(w) humanDoWhat(m) 不同输出\nlisaeat rice lisawatch TV coding3mineat banana coding3minplay game java 中的多态有三个必要条件\n 继承 重写 父类引用指向子类对象 但是 go 没有继承、重写, go 作为一种优雅的语言, 给我们提供了这种解决方案,那就是鸭子类型:看起来 像鸭子, 那么它就是 鸭子!\n3.3.4 练习 # 练习题目-practice.go\n3.3.5 小结 # interface 在go中是一种神奇的存在,interface{} 可以代表所有类型的基类,interface 也可以定义为类的方法模板,只不过在Go中是隐式的实现。\n这是一种很奇妙的体验,以后在工作或实战中很快就会熟悉了。\n"},{"id":15,"href":"/3.grammar-advancement/3-4-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/","title":"3 4 异常处理","section":"3.grammar-advancements","content":"3.4 异常处理 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/3.grammar-advancement/3.4-errors\n 3.4.1 异常处理思想 # 在 go 语言里是没有 try catch 的概念的,因为 try catch 会消耗更多资源,而且不管从 try 里面哪个地方跳出来,都是对代码正常结构的一种破坏。\n所以 go 语言的设计思想中主张\n 如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回 nil 每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这种 if 语句术语叫卫述语句 所以异常应该总是掌握在我们的手上,保证每次操作产生的影响达到最小,保证程序即使部分地方出现问题,也不会影响整个程序的运行,及时的处理异常,这样就可以减轻上层处理异常的压力。\n同时也不要让未知的异常使你的程序崩溃。\n3.4.2 异常的形式 # 我们应该让异常以这样的形式出现\nfunc Demo() (int, error) 我们应该让异常以这样的形式处理(卫述语句)\n_,err := errorDemo() if err!=nil{ fmt.Println(err) return } 3.4.3 自定义异常 # 比如程序有一个功能为除法的函数,除数不能为 0 ,否则程序为出现异常,我们就要提前判断除数,如果为 0 返回一个异常。那他应该这么写。\nfunc divisionInt(a, b int) (int, error) { if b == 0 { return -1, errors.New(\u0026#34;除数不能为0\u0026#34;) } return a / b, nil } 这个函数应该被这么调用\na, b := 4, 0 res, err := divisionInt(a, b) if err != nil { fmt.Println(err.Error()) return } fmt.Println(a, \u0026#34;除以\u0026#34;, b, \u0026#34;的结果是 \u0026#34;, res) 可以注意到上面的两个知识点\n 创建一个异常 errors.New(\u0026quot;字符串\u0026quot;) 打印异常信息 err.Error() 只要记得这些,你就掌握了自定义异常的基本方法。\n但是 errors.New(\u0026quot;字符串\u0026quot;) 的形式我不建议使用,因为他不支持字符串格式化功能,所以我一般使用 fmt.Errorf 来做这样的事情。\nerr = fmt.Errorf(\u0026#34;产生了一个 %v 异常\u0026#34;, \u0026#34;喝太多\u0026#34;) 3.4.4 详细的异常信息 # 上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 errors 的内部实现了。其实相当简单。\nerrors 实现了一个叫 error 的接口,这个接口里就一个 Error 方法且返回一个 string ,如下\ntype error interface { Error() string } 只要结构体实现了这个方法就行,源码的实现方式如下\ntype errorString struct { s string } func (e *errorString) Error() string { return e.s } // 多一个函数当作构造函数 func New(text string) error { return \u0026amp;errorString{text} } 所以我们只要扩充下自定义 error 的结构体字段就行了。\n这个自定义异常可以在报错的时候存储一些信息,供外部程序使用\ntype FileError struct { Op string Name string Path string } // 初始化函数 func NewFileError(op string, name string, path string) *FileError { return \u0026amp;FileError{Op: op, Name: name, Path: path} } // 实现接口 func (f *FileError) Error() string { return fmt.Sprintf(\u0026#34;路径为 %v 的文件 %v,在 %v 操作时出错\u0026#34;, f.Path, f.Name, f.Op) } 调用\nf := NewFileError(\u0026#34;读\u0026#34;, \u0026#34;README.md\u0026#34;, \u0026#34;/home/how_to_code/README.md\u0026#34;) fmt.Println(f.Error()) 输出\n路径为 /home/how_to_code/README.md 的文件 README.md,在 读 操作时出错 3.4.5 defer # 上面说的内容很简单,在工作里也是最常用的,下面说一些拓展知识。\nGo 中有一种延迟调用语句叫 defer 语句,它在函数返回时才会被调用,如果有多个 defer 语句那么它会被逆序执行。\n比如下面的例子是在一个函数内的三条语句,他是这么怎么执行的呢?\ndefer fmt.Println(\u0026#34;see you next time!\u0026#34;) defer fmt.Println(\u0026#34;close all connect\u0026#34;) fmt.Println(\u0026#34;hei boy\u0026#34;) 输出如下, 可以看到两个 defer 在程序的最后才执行,而且是逆序。\nhei boy close all connect see you next time! 这一节叫异常处理详解,终归是围绕异常处理来讲述知识点, defer 延迟调用语句的用处是在程序执行结束,甚至是崩溃后,仍然会被调用的语句,通常会用来执行一些告别操作,比如关闭连接,释放资源(类似于 c++ 中的析构函数)等操作。\n涉及到 defer 的操作\n 并发时释放共享资源锁 延迟释放文件句柄 延迟关闭 tcp 连接 延迟关闭数据库连接 这些操作也是非常容易被人忘记的操作,为了保证不会忘记,建议在函数的一开始就放置 defer 语句。\n3.4.6 panic # 刚刚有说到 defer 是崩溃后,仍然会被调用的语句,那程序在什么情况下会崩溃呢?\nGo 的类型系统会在编译时捕获很多异常,但有些异常只能在运行时检查,如数组访问越界、空指针引用等。这些运行时异常会引起 painc 异常(程序直接崩溃退出)。然后在退出的时候调用当前 goroutine 的 defer 延迟调用语句。\n有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候)\ndefer fmt.Println(\u0026#34;关闭文件句柄\u0026#34;) panic(\u0026#34;人工创建的运行时异常\u0026#34;) 报错如下\n 3.4.7 panic recover # 出现 panic 以后程序会终止运行,所以我们应该在测试阶段发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的web或者rpc服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。\n然后再借助运维监控系统对日志的监控,发送告警给运维、开发人员,进行紧急修复。\n语法如下:\nfunc divisionIntRecover(a, b int) (ret int) { defer func() { if err := recover(); err != nil { // 打印异常,关闭资源,退出此函数 \tfmt.Println(err) ret = -1 } }() return a / b } 调用\nvar res int datas := []struct { a int b int }{ {2, 0}, {2, 2}, } for _, v := range datas { if res = divisionIntRecover(v.a, v.b); res == -1 { continue } fmt.Println(v.a, \u0026#34;/\u0026#34;, v.b, \u0026#34;计算结果为:\u0026#34;, res) } 输出结果\nruntime error: integer divide by zero 2 / 2 计算结果为: 1 调用 panic 后,当前函数从调用点直接退出 recover 函数只有在 defer 代码块中才会有效果 recover 可以放在最外层函数,做统一异常处理。 3.4.8 小结 # defer和panic是面试的高频题,因为在工作中非常常用。\n自定义错误的语法在正规项目中最为常用,可以和我一起实战一定能体验到了。\n"},{"id":16,"href":"/4.concurrent/4-1-go%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%B9%B6%E5%8F%91%E7%89%B9%E6%80%A7/","title":"4 1 Go语言中的并发特性","section":"4.concurrents","content":"4.1 go 语言中的并发特性 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.1-goroutine/\n 以前我们写并发的程序一般是用多线程来实现,自己维护一个线程池,在恰当的时候创建、销毁、分配资源。\ngo 在并发方面为我们提供了一个语言级别的支持, goroutine 和 channel 相互配合,这决定了他的先天优势。\ngoroutine 也就是go协程,概念类似于线程, Go 程序运行时会自动调度和管理,系统能智能地将 goroutine 中的任务合理地分配给 CPU , 让这些任务尽量并发运作。\n4.1.1 他和线程对比 # 从使用上讲\n 比线程更轻量级,可以创建十万、百万不用担心资源问题。 和 channel 搭配使用,实现高并发, goroutine 之间传输数据更方便。 如果访问同一个数据块,要小心数据竞态问题、共享锁还是互斥锁的选择问题、并发操作的数据同步问题(后面会说) 从其实现上讲\n 从资源上讲,线程的栈内存大小一般是固定的一般为 2MB ,虽然这个数值可以设置,但是 太大了浪费,太小了容易不够用, 而 goroutine 栈内存是可变的,初始一般为 2KB ,随着需求可以扩大达到 1GB。 所以 goroutine 十分的轻量级,且能满足不同的需求。 从调度上讲,线程的调度由 OS 的内核完成;线程的切换需要 CPU 寄存器 和 内存的数据交换 ,从而切换不同的线程上下文。 其触发方式为 CPU时钟 , 而 goroutine 的调度则比较轻量级,由自身的调度器完成。 协程同线程的关系,有些类似于 线程同进程的关系。 4.1.2 创建与使用 # 创建一个 goroutine ,只需要在函数前加一个 go 关键字就成了。\ngo 函数名(参数) 看一个 dome\nfunc quickFun(){ fmt.Println(\u0026#34;maybe you can\u0026#39;s see me!\u0026#34;) } func main(){ go quickFun() // 创建了一个 goroutine \tfmt.Println(\u0026#34;hey\u0026#34;) time.Sleep(time.Second) } goroutine 和 main 主线程同时运行 main 运行结束会暴力终止所有协程,所以上面的程序多等待了 1 秒 Go 程序从 main 包的 main() 函数开始,在程序启动时, Go 程序就会为 main() 函数创建一个默认的 goroutine 。 输出\nhey maybe you can's see me! 对,就是这么简单,如果你的函数只在这里使用,也可以用匿名函数来创建 goroutine 。\nfunc main(){ go func() { fmt.Println(\u0026#34;hello \u0026#34;) }() time.Sleep(time.Second) //main运行结束会暴力终止所有协程,所以这里先等待1秒 } PS: 和线程不同,goroutine没有唯一的id,所以我们没办法专门针对某个协程进行操作。\n4.1.3 体验并发 # 当执行 goroutine 时候,Go 语言立即返回,接着执行剩余的代码,不会阻塞主线程。\n下面我们通过一小段代码来讲解 go 的使用:\n//首先我们先实现一个 Add()函数 func Add(a, b int) { c := a + b fmt.Println(c) } go Add(1, 2) //使用go关键字让函数并发执行 该函数就会在一个新的 goroutine 中并发执行,当该函数执行完毕时,这个新的 goroutine 也就结束了。\n不过需要注意的是,如果该函数具有返回值,那么返回值会被丢弃。所以什么时候用 go 还需要酌情考虑。\n接着我们通过一个案例来体验一下 Go 的并发到底是怎么样的。新建源文件 goroutine2.go,输入以下代码:\npackage main import \u0026#34;fmt\u0026#34; func Add(a, b int) { c := a + b fmt.Println(c) } func main() { for i := 0; i \u0026lt; 10; i++ { go Add(i, i) } } 执行 goroutine.go 文件会发现屏幕上什么都没有,但程序并不会报错,这是什么原因呢?\n原来当主程序执行到 for 循环时启动了 10 个 goroutine,然后主程序就退出了,而启动的 10 个 goroutine 还没来得及执行 Add() 函数,所以程序不会有任何输出。也就是说主 goroutine 并不会等待其他 goroutine 执行结束。\n并发等待的问题我将在下一节进行介绍。\n4.1.4 小结 # 学 go 语言必学并发,通过本节我们知道了 goroutine 是 Go 语言并行设计的核心。十几个 goroutine 可能在底层就是几个线程。 实际上是 Go 在 runtime 系统调用等多方面对 goroutine 调度进行了封装和处理。\n协程是非常容易创建的,而且他非常轻量只占用2k,其他语言最小大多都是 MB,协程的使用还要配合数据传输,生产者消费者模型,关于协程的调度,我们后续再说。\n另外并发 bug 的定位和解决是老大难的问题了,平时就要注意的良好的代码风格和编程习惯。\n拓展知识(栈内存):\n关于 goroutine stack size(栈内存大小) 官方的文档 中所述,1.2 之前最小是4kb,在1.2 变成8kb,并且可以使用 SetMaxStack 设置栈最大大小。\n在 runtime/debug 包能控制最大的单个 goroutine 的堆栈的大小。在 64 位系统上默认为 1GB,在 32 位系统上默认为 250MB。\n因为每个goroutine需要能够运行,所以它们都有自己的栈。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法存在许多的goroutine。\n所以在 1.3版本中,改为了 Contiguous stack( 连续栈 ),为了解决这个问题,goroutine可以初始时只给栈分配很小的空间(8KB),然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。\n 1.4 版本 goroutine 堆栈从 8Kb 减少到 2Kb\n拓展阅读\n 连续栈 Go: How Does the Goroutine Stack Size Evolve? "},{"id":17,"href":"/4.concurrent/4-2-goroutine-wait/","title":"4 2 Goroutine Wait","section":"4.concurrents","content":"4.2 并发等待 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.2-goroutine-wait/\n 4.2.1 简介 # goroutine 是 Golang 中非常有用的功能,有时候 goroutine 没执行完函数就返回了,如果希望等待当前的 goroutine 执行完成再接着往下执行,该怎么办?\nfunc say(s string) { for i := 0; i \u0026lt; 3; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say(\u0026#34;hello world\u0026#34;) fmt.Println(\u0026#34;over!\u0026#34;) } 输出 over! , 主线程没有等待\n4.2.2 使用 Sleep 等待 # func main() { go say(\u0026#34;hello world\u0026#34;) time.Sleep(time.Second*1) fmt.Println(\u0026#34;over!\u0026#34;) } 运行修改后的程序,结果如下:\nhello world hello world hello world over! 结果符合预期,但是太 low 了,我们不知道实际执行中应该等待多长时间,所以不能接受这个方案!\n4.2.3 发送信号 # func main() { done := make(chan bool) go func() { for i := 0; i \u0026lt; 3; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(\u0026#34;hello world\u0026#34;) } done \u0026lt;- true }() \u0026lt;-done fmt.Println(\u0026#34;over!\u0026#34;) } 输出的结果和上面相同,也符合预期\n这种方式不能处理多个协程,所以也不是优雅的解决方式。\n4.2.4 WaitGroup # Golang 官方在 sync 包中提供了 WaitGroup 类型可以解决这个问题。其文档描述如下:\n使用方法可以总结为下面几点:\n 在父协程中创建一个 WaitGroup 实例,比如名称为:wg 调用 wg.Add(n) ,其中 n 是等待的 goroutine 的数量 在每个 goroutine 运行的函数中执行 defer wg.Done() 调用 wg.Wait() 阻塞主逻辑 直到所有 goroutine 执行完成。 func main() { var wg sync.WaitGroup wg.Add(2) go say2(\u0026#34;hello\u0026#34;, \u0026amp;wg) go say2(\u0026#34;world\u0026#34;, \u0026amp;wg) fmt.Println(\u0026#34;over!\u0026#34;) wg.Wait() } func say2(s string, waitGroup *sync.WaitGroup) { defer waitGroup.Done() for i := 0; i \u0026lt; 3; i++ { fmt.Println(s) } } 输出,注意顺序混乱是因为并发执行\nhello hello hello over! world world world 4.2.5 小心缺陷 # 简短的例子,注意循环传入的变量用中间变量替代,防止闭包 bug\nfunc errFunc() { var wg sync.WaitGroup sList := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;} wg.Add(len(sList)) for _, d := range sList { go func() { defer wg.Done() fmt.Println(d) }() } wg.Wait() } 输出,可以发现全部变成了最后一个\nb b 父协程与子协程是并发的。父协程上的for循环瞬间执行完了,内部的协程使用的是d最后的值,这就是闭包问题。\n解决方法当作参数传入\nfunc correctFunc() { var wg sync.WaitGroup sList := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;} wg.Add(len(sList)) for _, d := range sList { go func(str string) { defer wg.Done() fmt.Println(str) }(d) } wg.Wait() } 输出\nb a 要留意 range 中的value有可能出现 1.7.3 有可能会遇到的坑!\n引用 # Golang 入门 : 等待 goroutine 完成任务\n"},{"id":18,"href":"/4.concurrent/4-3-channel/","title":"4 3 Channel","section":"4.concurrents","content":"4.3 channel # 到这里你正在接触最核心和重要的知识!认真学习的你很棒!\n 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.3-channel/\n 4.3.1 什么是 channel # Go 是一门从语言级别就支持并发的编程语言, 它有一个设计哲学很特别 不要通过共享内存来通信,而应通过通信来共享内存 ,听起来是有一点绕。\n在传统语言中并发使用全局变量来进行不同线程之间的数据共享,这种方式就是使用共享内存的方式进行通信。而 Go 会在协程和协程之间打一个隧道,通过这个隧道来传输数据(发送和接收)。\n 打个比方,我们平时肯定没少接触过队列,队列的特点是先进先出,多方生产插入,多方消费接收。这个队列/隧道就是channel。\nchannel 是 goroutine 之间互相通讯的东西,goroutine 之间用来发消息和接收消息。其实,就是在做 goroutine 之间的内存共享。\n我们来看看具体是什么使用的。\n4.3.2 声明与初始化 # channel是类型相关的,也就是说一个 channel 只能传递一种类型的值,这个类型需要在 channel 声明时指定。\nchannel 的一般声明形式:\nvar chanName chan 类型 与普通变量的声明不同的是在类型前面加了 channel 关键字,类型 则指定了这个 channel 所能传递的元素类型。示例:\nvar a chan int //声明一个传递元素类型为int的channel var b chan float64 var c chan string 通道是一个引用类型,初始值为nil,对于值为nil的通道,不论具体是什么类型,它们所属的接收和发送操作都会永久处于阻塞状态。\n所以必须手动make初始化,示例:\na := make(chan int) //初始化一个int型的名为a的channel b := make(chan float64) c := make(chan string) 既然是队列,那就有大小,上面没声明具体的大小,被认为是无缓冲的(注意大小是 0,不是 1)也就是说必须有其他goroutine接收,不然就会阻塞在那。声明有缓冲的,指定大小就可以了。\na := make(chan int,100) 4.3.3 如何使用 # 我们进一步体验一下无缓冲 channel 会发生什么问题,同时熟悉下用法,示例:\nfunc pendingForever() { a := make(chan int) a \u0026lt;- 1 //将数据写入channel z := \u0026lt;-a //从channel中读取数据 fmt.Println(z) } 观察上面三行代码,第 2 行往 channel 内写入了数据,第 3 行从 channel 中读取了数据 但是这是在同一个方法中,并且没有使用 Go 关键字,说明他们在同一个协程 我们说过 channel 是用来给不同 goroutine 通信的,所以是不能在同一个协程又发送又接收,这根本就达不到隧道通信的效果。所以上面的代码,会死锁:\nfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() .../4.concurrent/channel.go:7 +0x59 死锁的原因是没有其他协程来接收数据,隧道因为是无缓冲的,所以直接永远的阻塞在发送方。\n要解决这个问题也好办。放到不同 goroutine 里就可以。\nfunc normal() { chanInt := make(chan int) go func() { chanInt \u0026lt;- 1 }() res := \u0026lt;-chanInt fmt.Println(res) } 输出1。无缓冲通道在无数据发送时,接收端会阻塞,直到有新数据发送过来为止。\n上面的代码,一个发送一个接收,而实际使用中数据往往是连续不断发送的。来看一段代码:\nfunc standard() { chanInt := make(chan int) go func() { defer close(chanInt) var produceData = []int{1, 2, 3} for _, v := range produceData { chanInt \u0026lt;- v } }() for v := range chanInt { fmt.Println(v) } } 输出\n1 2 3 循环传递数据,父协程循环接收。 range chan 的方式可以不断的接收数据,直到通道关闭,假如通道不关闭会永远阻塞,无法通过编译,直接报死锁。 必须在发送端关闭通道,因为接收端无法预料是否还有数据没有接收完;向已关闭的channel发送数据会panic。 建议使用 defer 来关闭通道,防止程序异常时未正常关闭。 至此我们完成了一个简单的生产者消费者模型。\n4.3.4 channel 的关闭 # 使用 Go 语言内置的 close() 函数即可关闭 channel,再强调一次建议使用defer关闭,示例:\ndefer close(ch) 关闭了 channel 后如何查看 channel 是否关闭成功了呢?很简单,我们可以在读取 channel 时采用多重返回值的方式,示例:\nx, ok := \u0026lt;-ch 通过查看第二个返回值的 bool 值即可判断 channel 是否关闭,若为 false 则表示 channel 被关闭,反之则没有关闭(使用频率不高,了解即可)\nfunc main() { var chanInt chan int = make(chan int, 10) go func() { defer fmt.Println(\u0026#34;chanInt is closed\u0026#34;) defer close(chanInt) chanInt \u0026lt;- 1 }() res := \u0026lt;-chanInt fmt.Println(res) } 输出\nchanInt is closed 1 如上声明了一个有缓冲的通道,在缓冲大小允许的范围内不需要阻塞等待接收 发送端发送完毕后主动关闭通道 虽然通道已经关闭,接收端依然可以接收,接收完自行结束。 PS1: 同一个通道只能关闭一次,重复关闭会panic。\nPS2: 如果传入nil,如 close(nil) 会 panic。\n4.3.5 多发送、多接收与单向通道 # 我们结合前面知识,来实战练习一下!\n功能:实现一个多发送,多接收的例子。\nfunc send(c chan\u0026lt;- int, wg *sync.WaitGroup) { c \u0026lt;- rand.Int() wg.Done() } 发送端随机生成数字,并声明一个仅发送的单向通道 使用sync.WaitGroup做等待(忘记的回顾上一节哈!) func received(c \u0026lt;-chan int, wg *sync.WaitGroup) { for gotData := range c { fmt.Println(gotData) } wg.Done() } 接收端使用range来接收数字并打印 func main() { chanInt := make(chan int, 10) done := make(chan struct{}) defer close(done) go func() { defer close(chanInt) // 发送 }() go func() { ... // 接收 done \u0026lt;- struct{}{} }() \u0026lt;-done } 使用了两个通道,一个通道chanInt进行数据传输,另一个done控制完毕时结束主协程 发送端负责生产数据,生产完毕后关闭通道 接收端负责接收完毕后通知主协程 发送端\ngo func() { var wg sync.WaitGroup defer close(chanInt) for i := 0; i \u0026lt; 5; i++ { wg.Add(1) go send(chanInt, \u0026amp;wg) } wg.Wait() }() 连续启动 5 个协程,使用wg做协程等待,发送完毕再结束是为了交给defer关闭chanInt\n接收端\ngo func() { var wg sync.WaitGroup for i := 0; i \u0026lt; 8; i++ { wg.Add(1) go received(chanInt, \u0026amp;wg) } wg.Wait() done \u0026lt;- struct{}{} }() 连续启动多个接收端,通道被关闭时纷纷退出,最后通知done\n输出 5 个随机数,程序正常关闭。\n5577006791947779410 8674665223082153551 4037200794235010051 6129484611666145821 3916589616287113937 单向通道限制了函数的使用方式,它可以用在循环比较耗时的场景,处理完一个数据立马发送出来,尽量减少内存的使用。\n4.3.6 小结 # 这一节简单介绍了 go 语言中的 channel(信道),go 语言主张不要通过共享内存来通信,而应通过通信来共享内存,通过channel的方式可以完成不同goroutine之间的通信。\n我们学会了:\n channel 是引用类型默认值是nil,需要手动make。 通道必须在多个goroutine中使用 有缓冲与无缓冲通道的特点,什么时候会阻塞。 可以用range来做循环接收,通道关闭会自动停止。 只能且必须在发送端使用defer关闭通道。 正式使用一般多发送多接收,并使用done信号通知的方式进行通知。 在工作中,通道的使用更为复杂,下一节将介绍两个面试高频的问题,敬请期待!\n"},{"id":19,"href":"/4.concurrent/4-4-deadlock/","title":"4 4 Deadlock","section":"4.concurrents","content":"4.4 deadlock # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.4-deadlock/\n 4.4.1 什么时候会导致死锁 # 在计算机组成原理里说过 死锁有三个必要条件他们分别是 循环等待、资源共享、非抢占式,在并发中出现通道死锁只有两种情况:\n 数据要发送,但是没有人接收 数据要接收,但是没有人发送 4.4.2 发送单个值时的死锁 # 牢记这两点问题就很清晰了,复习下之前的例子,会死锁\na := make(chan int) a \u0026lt;- 1 //将数据写入channel z := \u0026lt;-a //从channel中读取数据 有且只有一个协程时,无缓冲的通道 先发送会阻塞在发送,先接收会阻塞在接收处。 发送操作在接收者准备好之前是阻塞的,接收操作在发送之前是阻塞的, 解决办法就是改为缓冲通道,或者使用协程配对 解决方法一,协程配对,先发送还是先接收无所谓只要配对就好\nchanInt := make(chan int) go func() { chanInt \u0026lt;- 1 }() res := \u0026lt;-chanInt 解决方法二,缓冲通道\nchanInt := make(chan int,1) chanInt \u0026lt;- 2 res := \u0026lt;-chanInt 缓冲通道内部的消息数量用len()函数可以测试出来 缓冲通道的容量可以用cap()测试出来 在满足cap\u0026gt;len时候,因为没有满,发送不会阻塞 在len\u0026gt;0时,因为不为空,所以接收不会阻塞 使用缓冲通道可以让生产者和消费者减少阻塞的可能性,对异步操作更友好,不用等待对方准备,但是容量不应设置过大,不然会占用较多内存。\n4.4.3 多个值发送的死锁 # 配对可以让死锁消失,但发送多个值的时候又无法配对了,又会死锁\nfunc multipleDeathLock() { chanInt := make(chan int) defer close(chanInt) go func() { res := \u0026lt;-chanInt fmt.Println(res) }() chanInt \u0026lt;- 1 chanInt \u0026lt;- 1 } 不出所料死锁了\nfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.multipleDeathLock() 在工作中只有通知信号是一对一的情况,通知一次以后就不再使用了,其他这种要求多次读写配对的情况根本不会存在。\n4.4.4 解决多值发送死锁 # 更常见的是用循环来不断接收值,接受一个处理一个,如下:\nfunc multipleLoop() { chanInt := make(chan int) defer close(chanInt) go func() { for { //不使用ok会goroutine泄漏 \t//res := \u0026lt;-chanInt \tres,ok := \u0026lt;-chanInt if !ok { break } fmt.Println(res) } }() chanInt \u0026lt;- 1 chanInt \u0026lt;- 1 } 输出:\n1 1 给通道的接收加上二值,ok 代表通道是否正常,如果是关闭则为false值 可以删掉那段逻辑试试,会输出1 2 0 0 0这样的数列,因为关闭是需要时间的,而循环接收关闭的通道拿到的是0 在有缓冲的channel中,虽然通道关闭了,但直到读取完成所有数据才会输出0值,以及二值时返回false 关于goroutine泄漏稍后会讲到 4.4.5 应该先发送还是先接收 # 假如我们调换一下位置,把接收放外面,写入放里面会发生什么\nfunc multipleDeathLock2() { chanInt := make(chan int) defer close(chanInt) go func() { chanInt \u0026lt;- 1 chanInt \u0026lt;- 2 }() for { res, ok := \u0026lt;-chanInt if !ok { break } fmt.Println(res) } } 输出死锁\n1 2 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.multipleDeathLock2() 出现上面的结果是因为for循环一直在获取通道中的值,但是在读取完1 2后,通道中没有新的值传入,这样接收者就阻塞了。 为什么先接收再发送可以,因为发送提前结束后会触发函数的defer自动关闭通道 所以我们应该总是先接收后发送,并由发送端来关闭 4.4.6 goroutine 泄漏 # goroutine 终止的场景有三个:\n 当一个 goroutine 完成了它的工作 由于发生了没有处理的错误 有其他的协程告诉它终止 当三个条件都没有满足,goroutine 就会一直运行下去\nfunc goroutineLeak() { chanInt := make(chan int) defer close(chanInt) go func() { for { res := \u0026lt;-chanInt //res,ok := \u0026lt;-chanInt \t//if !ok { // break //} \tfmt.Println(res) } }() chanInt \u0026lt;- 1 chanInt \u0026lt;- 1 } 上面的goroutineLeak()函数结束后触发defer close(chanInt)关闭了通道 但是匿名函数中goroutine并没有关闭,而是一直在循环取值,并且取到是的关闭后的通道值(这里是int的默认值 0) goroutine会永远运行下去,如果以后再次使用又会出现新的泄漏!导致内存、cpu占用越来越多 输出,如果程序不停止就会一直输出0\n1 1 0 0 0 ... 假如不关闭且外部没有写入值,那接收处就会永远阻塞在那里,连输出都不会有\nfunc goroutineLeakNoClosed() { chanInt := make(chan int) go func() { for { res := \u0026lt;-chanInt fmt.Println(res) } }() } 无任何输出的阻塞 换成写入也是一样的 如果是有缓冲的通道,换成已满的通道写没有读;或者换成向空的通道读没有写也是同样的情况 除了阻塞,goroutine进入死循环也是泄露的原因 4.4.7 如何发现泄露 # 使用 golang 自带的pprof监控工具,可以发现内存上涨情况,这个后续会讲\n还可以监控进程的内存使用情况,比如prometheus提供的process-exporter\n如果你有内存泄露/goroutine 泄露代码扫描的工具,欢迎留言,感恩!\n4.4.8 小结 # 今天我们学习了一些细节,但是相当重要的知识点,也是未来面试高频问题哦!\n 如果是信号通知,应该保证一一对应,不然会死锁 除了信号通知外,通常我们使用循环处理通道,在工作中不断的处理数据 应该总是先接收后发送,并由发送端来关闭,不然容易死锁或者泄露 在接收处,应该对通道是否关闭做好判断,已关闭应该退出接收,不然会泄露 小心 goroutine 泄漏,应该在通道关闭的时候及时检查通道并退出 除了阻塞,goroutine进入死循环也是泄露的原因 "},{"id":20,"href":"/4.concurrent/4-5-select/","title":"4 5 Select","section":"4.concurrents","content":"4.4 select # \u0026hellip;本节正在编写,未完待续,催更请留言,我会收到邮件 # 本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.5-select\n 4.3.1 select与switch # 让我们来复习一下switch语句,在switch语句中,会逐个匹配case语句(可以是值也可以是表达式),一个一个的判断过去,直到有符合的语句存在,执行匹配的语句内容后跳出switch。\nfunc demo(number int){ switch{ case number \u0026gt;= 90: fmt.Println(\u0026#34;优秀\u0026#34;) default: fmt.Println(\u0026#34;太搓了\u0026#34;) } } 而 select 用于处理通道,它的语法与 switch 非常类似。\nfunc main() { chanInt1, chanInt2 := make(chan int), make(chan int) go func() { defer close(chanInt1) defer close(chanInt2) chanInt1 \u0026lt;- 1 chanInt2 \u0026lt;- 2 }() time.Sleep(time.Millisecond) select { case data := \u0026lt;-chanInt1: fmt.Println(data) case data := \u0026lt;-chanInt2: fmt.Println(data) default: fmt.Println(\u0026#34;全部阻塞\u0026#34;) } } 比如这样接收数值\n4.3.4 超时机制 # 相反的,如果 channel 中的数据一直没有被读取,那么写入操作也会一直处于阻塞状态。如果不正确处理这个情况,很可能会导致整个 goroutine 锁死,这就是超时问题。Go 语言没有针对超时提供专门的处理机制,但是我们却可以利用 select 来巧妙地实现超时处理机制,下面看一个示例:\nt := make(chan bool) go func() { time.Sleep(1e9) //等待1秒 t \u0026lt;- true }() select { case \u0026lt;-ch: //从ch中读取数据 case \u0026lt;-t: //如果1秒后没有从ch中读取到数据,那么从t中读取,并进行下一步操作 } 这样的方法就可以让程序在等待 1 秒后继续执行,而不会因为 ch 读取等待而导致程序停滞,从而巧妙地实现了超时处理机制,这种方法不仅简单,在实际项目开发中也是非常实用的。\n4.3.2 select # 由 select 开始一个新的选择块,每个选择条件由 case 语句来描述,并且每个 case 语句里必须是一个 channel 操作。它既可以用于 channel 的数据接收,也可以用于 channel 的数据发送。如果 select 的多个分支都满足条件,则会随机的选取其中一个满足条件的分支。\n新建源文件 channel.go,输入以下代码:\nfunc main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(time.Second * 1) c1 \u0026lt;- \u0026#34;one\u0026#34; }() go func() { time.Sleep(time.Second * 2) c2 \u0026lt;- \u0026#34;two\u0026#34; }() start := time.Now() // 获取当前时间 for i := 0; i \u0026lt; 2; i++ { select { case msg1 := \u0026lt;-c1: fmt.Println(\u0026#34;received\u0026#34;, msg1) case msg2 := \u0026lt;-c2: fmt.Println(\u0026#34;received\u0026#34;, msg2) } } elapsed := time.Since(start) // 这里没有用到3秒,为什么? \tfmt.Println(\u0026#34;该函数执行完成耗时:\u0026#34;, elapsed) } 以上代码先初始化两个 channel c1 和 c2,然后开启两个 goroutine 分别往 c1 和 c2 写入数据,再通过 select 监听两个 channel,从中读取数据并输出。\n运行结果如下:\n$ go run channel.go received one received two 该函数执行完成耗时: 2.004695535s 泄露防止 # 及时通知select\n小结 # "},{"id":21,"href":"/4.concurrent/4-6-timeout/","title":"4 6 Timeout","section":"4.concurrents","content":"4.4 定时器 # \u0026hellip;本节正在编写,未完待续,催更请留言,我会收到邮件 # 超时关闭 # 完整代码\npackage main import \u0026#34;time\u0026#34; func main() { t := make(chan bool) ch := make(chan int) defer func() { close(ch) close(t) }() go func() { time.Sleep(1e9) //等待1秒 \tt \u0026lt;- true }() go func() { time.Sleep(time.Second * 2) ch \u0026lt;- 123 }() select { case \u0026lt;-ch: //从ch中读取数据 case \u0026lt;-t: //如果1秒后没有从ch中读取到数据,那么从t中读取,并进行下一步操作 \t} } 可热更新的定时器 # 废话不多说,直接上代码\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type Server struct { tk *time.Ticker reset chan struct{} Close chan struct{} Period int64 } func main() { s := CreateServer(1) go s.Start() time.Sleep(time.Duration(10) * time.Second) s.Update(3) time.Sleep(time.Duration(10) * time.Second) s.Stop() fmt.Println(\u0026#34;good bye\u0026#34;) } func CreateServer(Period int64) *Server { return \u0026amp;Server{ tk: nil, reset: make(chan struct{}), Close: make(chan struct{}), Period: Period, } } // 程序启动 func (s *Server) Start() { // 定时 \ts.tk = time.NewTicker(time.Duration(s.Period) * time.Second) defer s.tk.Stop() for { select { case \u0026lt;-s.Close: return case \u0026lt;-s.tk.C: fmt.Println(\u0026#34;定时唤醒:\u0026#34;, time.Now().Format(\u0026#34;2006-01-02 15:04:05\u0026#34;)) case \u0026lt;-s.reset: s.tk.Stop() s.tk = time.NewTicker(time.Duration(s.Period) * time.Second) } } } func (s *Server) Stop() { close(s.Close) close(s.reset) } func (s *Server) Update(p int64) { s.Period = p s.reset \u0026lt;- struct{}{} } \u0026hellip;未完待续,催更请留言,我会收到邮件\n"},{"id":22,"href":"/4.concurrent/4-7-%E6%B1%A0/","title":"4 7 池","section":"4.concurrents","content":"\u0026hellip;本节正在编写,未完待续,催更请留言,我会收到邮件 # "},{"id":23,"href":"/4.concurrent/%E5%AE%9E%E6%88%98-%E6%97%A5%E5%BF%97%E7%9B%91%E6%8E%A7/","title":"实战 日志监控","section":"4.concurrents","content":"\u0026hellip;本节正在编写,未完待续,催更请留言,我会收到邮件 # "},{"id":24,"href":"/books-share/","title":"Books Share","section":"","content":"书籍推荐 # 我读过或者我正在读,觉得不错的书,推荐给大家,欢迎评论推荐或者留下在线书籍链接\nPS:仅供学习,请勿传播\n入门 # 《go语言圣经》 实战(建议深入学习前部分实战拜读) # 《go web编程实战派》 比较基础和全面 《go语言编程之旅》蓝色封面,2020年出的书 更适合有一定基础的人 进阶 # 《go专家编程》 《go语言并发之道》 《go并发编程实战》 《Go语言高并发与微服务实战》 深入 # 《深入解析Go》 底层研究 《极客时间go语言36讲》对新人不友好,逻辑有些混乱,比较熟悉语言时再拜读,我读了一半实在看不下去 "},{"id":25,"href":"/goland/","title":"Goland","section":"","content":"Goland常用快捷键 # 文件相关快捷键:\n CTRL+E,打开最近浏览过的文件。 CTRL+SHIFT+E,打开最近更改的文件。 CTRL+N,可以快速打开struct结构体。 CTRL+SHIFT+N,可以快速打开文件。 代码格式化:\n CTRL+ALT+T,可以把代码包在一个块内,例如if{…}else{…}。 CTRL+ALT+L,格式化代码。 CTRL+空格,代码提示。 CTRL+/,单行注释。CTRL+SHIFT+/,进行多行注释。 CTRL+B,快速打开光标处的结构体或方法(跳转到定义处)。 CTRL+“+/-”,可以将当前方法进行展开或折叠。 查找和定位\n CTRL+R,替换文本。 CTRL+F,查找文本。 CTRL+SHIFT+F,进行全局查找。 CTRL+G,快速定位到某行。 代码编辑\n ALT+Q,可以看到当前方法的声明。 CTRL+Backspace,按单词进行删除。 SHIFT+ENTER,可以向下插入新行,即使光标在当前行的中间。 CTRL+X,删除当前光标所在行。 CTRL+D,复制当前光标所在行。 ALT+SHIFT+UP/DOWN,可以将光标所在行的代码上下移动。 CTRL+SHIFT+U,可以将选中内容进行大小写转化。 "},{"id":26,"href":"/howtocontribute/","title":"How to Contribute","section":"","content":"如何贡献项目 # 首先非常感谢你愿意参与贡献这个项目,让我们一起努力越做越好。\n参与贡献你可以参考下面的方法\nfork clone config # 在 GitHub 上fork到自己的仓库,如 xxx/golang,然后clone到本地,并设置用户信息。\n$ git clone git@github.com:xxx/golang.git $ cd golang $ git config user.name \u0026#34;yourname\u0026#34; $ git config user.email \u0026#34;your email\u0026#34; commit push # 修改代码后提交,并推送到自己的仓库。\n$ #do some change on the content $ git commit -m \u0026#34;Fix issue #1: change helo to hello\u0026#34; $ git push pr (pull request) # 在 GitHub 网站上提交 pull request。 当然了,如果你不会提pr,你可以参考我 给开源大项目贡献代码的文章\n到这里就完成贡献的整个过程了。\n同步代码 # 可以定期使用项目仓库内容更新自己仓库内容。\n$ git remote add upstream https://github.com/golang-minibear2333/golang $ git fetch upstream $ git checkout master $ git rebase upstream/master $ git push -f origin master 这样就可以把我以后的更新同步到你本地啦~!\n丰富的贡献方式 # 其实你也不必提交代码来贡献,如果你发现项目中有任何不足、bug,或者疑问、新需求,你可以通过issue的方式让我提出。 我看到了会立刻给你回复\n甚至你可以直接在电子书底部,直接点击Edit this page的链接,修改完毕后参考我的commit提交格式,提交后会自动fork到你的github里,此时直接发起pr即可!\n"},{"id":27,"href":"/impossible/range/readme/","title":"Readme","section":"Impossibles","content":"for range的一个坑 # for range是值拷贝出来的副本\n在使用for range的时候,要注意的是,不管是slice还是map,循环的值都是被range值拷贝出来的副本值。 举个简单的例了\n对于list\nvar t []Test t = append(t, Test{Index: 1, Num: 1}) t = append(t, Test{Index: 2, Num: 2}) // 实际上没有成功修改t.Num,因为是副本复制 \tfor _, v := range t { v.Num += 100 } for _, v := range t { // 输出 \t// 1 1 \t// 2 2 \tfmt.Println(v.Index, v.Num) } 对于 map, 也不能这么搞,实际上都是复制\nm := make(map[int]Test) m[0] = Test{Index: 1, Num: 1} m[1] = Test{Index: 2, Num: 2} for _, v := range m { v.Num += 100 } for _, v := range m { // 输出(可以乱序) \t// 1 1 \t// 2 2 \tfmt.Println(v.Index, v.Num) } 怎么做? # 两个办法,用下标(map也一样)\nfor i := range t { t[i].Num += 100 fmt.Println(t[i].Num) // 输出(可以乱序) \t// 101 102 \t} 用指针\nvar t2 []*Test t2 = append(t2, \u0026amp;Test{Index: 1, Num: 1}) t2 = append(t2, \u0026amp;Test{Index: 2, Num: 2}) for k, v := range t2 { v.Num += 100 fmt.Println(t2[k].Num) // 输出(可以乱序) \t// 101 102 \t} for range 原理 # 通过查看 源代码 ,我们可以发现for range的实现是:\n# statements.cc:6419 (441f3f1 on 4 Oct) // Arrange to do a loop appropriate for the type. We will produce // for INIT ; COND ; POST { // ITER_INIT // INDEX = INDEX_TEMP // VALUE = VALUE_TEMP // If there is a value // original statements // } 并且对于Slice,Map等各有具体不同的编译实现,我们先看看for range slice的具体实现\n# statements.cc:6638 (441f3f1 on 4 Oct) // The loop we generate: // for_temp := range // len_temp := len(for_temp) // for index_temp = 0; index_temp \u0026lt; len_temp; index_temp++ { // value_temp = for_temp[index_temp] // index = index_temp // value = value_temp // original body // } 先是对要遍历的 Slice 做一个拷贝,获取长度大小,然后使用常规for循环进行遍历,并且返回值的拷贝。 再看看for range map的具体实现:\n# statements.cc:6891 (441f3f1 on 4 Oct) // The loop we generate: // var hiter map_iteration_struct // for mapiterinit(type, range, \u0026amp;hiter); hiter.key != nil; mapiternext(\u0026amp;hiter) { // index_temp = *hiter.key // value_temp = *hiter.val // index = index_temp // value = value_temp // original body // } 也是先对map进行了初始化,因为map是hashmap,所以这里其实是一个hashmap指针的拷贝。\n引用: Go 中for range的一个坑\n"},{"id":28,"href":"/impossible/%E5%88%9D%E5%AD%A6%E8%80%85%E5%B8%B8%E7%8A%AF%E7%9A%84%E9%94%99%E8%AF%AF/","title":"初学者常犯的错误","section":"Impossibles","content":"初学者常犯的错误 # 引用: Go 经典译文:50 个 Go 新手易犯的错误(2020版)\n 索引运算符和字符串 # 字符串上的 index 方法 (运算符) 返回一个字节值,而不是一个字符类型(就像在其他语言中一样)。\npackage main import \u0026#34;fmt\u0026#34; func main() { x := \u0026#34;text\u0026#34; fmt.Println(x[0]) //print 116 fmt.Printf(\u0026#34;%T\u0026#34;,x[0]) //prints uint8 } 如果需要访问特定字符串 “characters”(unicode 代码点 / 运行符),请使用 for range 语句。官方的 “unicode/utf8” 包和基础的 utf8string 包 (golang.org/x/exp/utf8string) 也很有用。utf8string 包有一个方便的 At() 方法,将字符串转换为切片也是一种选择。\n使用 「for range」子句遍历 Map # level:初学者 如果你希望 Map 每项数据按照顺序排列 (例如,按键值顺序),这是不可能的,每次 Map 迭代会输出不一样的结果。GO 运行时可能会随机分配迭代顺序,因此你可能会得到几次相同的 Map 迭代结果也不用惊讶。\npackage main import \u0026#34;fmt\u0026#34; func main() { m := map[string]int{\u0026#34;one\u0026#34;:1,\u0026#34;two\u0026#34;:2,\u0026#34;three\u0026#34;:3,\u0026#34;four\u0026#34;:4} for k,v := range m { fmt.Println(k,v) } } 而且,如果你使用 Go Playground ( play.golang.org/) 运行这段代码,将始终得到相同的迭代结果,因为除非进行更改代码,否则它不会重新编译代码。\n增量和减量 # 级别:初学者 许多语言都有递增和递减运算符。与其他语言不同,Go 不支持操作的前缀版本。你也不能在表达式中使用这两个运算符。\n失败:\npackage main import \u0026#34;fmt\u0026#34; func main(){ data := []int{1,2,3} i := 0 ++i //错误 fmt.Println(data [i++])//错误 } 编译错误:\n /tmp/sandbox101231828/main.go:8:语法错误:意外的 ++ /tmp/sandbox101231828/main.go:9:语法错误:意外的 ++,期望:\n 作品:\npackage main import \u0026#34;fmt\u0026#34; func main(){ data := []int{1,2,3} i := 0 i++ fmt.Println(data[i]) } 按位 NOT 运算符 # 级别:初学者 许多语言都使用〜作为一元 NOT 运算符 (也称为按位补码),但是 Go 为此重用了 XOR 运算符 (^)。\n失败:\npackage main import \u0026#34;fmt\u0026#34; func main(){ fmt.Println(〜2)//错误 } 编译错误:\n /tmp/sandbox965529189/main.go:6:按位补码运算符是 ^\n 作品:\npackage main import \u0026#34;fmt\u0026#34; func main(){ var d uint8 = 2 fmt.Printf(“%08b \\ n”,^ d) } Go 仍然使用 ^ 作为 XOR 运算符,这可能会使某些人感到困惑。\n如果你愿意,你可以用二进制的 XOR 操作 (例如,' NOT 0x02 \u0026lsquo;) 来表示一个单目的 NOT 操作 (例如,\u0026rsquo; 0x02 XOR 0xff \u0026lsquo;)。这可以解释为什么 ^ 被重用于表示一元 NOT 操作。\nGo 还具有一个特殊的 \u0026lsquo;AND NOT\u0026rsquo; 按位运算符 (\u0026amp;^),这增加了 NOT 运算符的困惑。看起来像一个特性 / 黑客,不需要括号就可以支持 A AND (NOT B)。\npackage main import \u0026#34;fmt\u0026#34; func main() { var a uint8 = 0x82 var b uint8 = 0x02 fmt.Printf(\u0026#34;%08b [A]\\n\u0026#34;,a) fmt.Printf(\u0026#34;%08b [B]\\n\u0026#34;,b) fmt.Printf(\u0026#34;%08b (NOT B)\\n\u0026#34;,^b) fmt.Printf(\u0026#34;%08b ^ %08b = %08b [B XOR 0xff]\\n\u0026#34;,b,0xff,b ^ 0xff) fmt.Printf(\u0026#34;%08b ^ %08b = %08b [A XOR B]\\n\u0026#34;,a,b,a ^ b) fmt.Printf(\u0026#34;%08b \u0026amp; %08b = %08b [A AND B]\\n\u0026#34;,a,b,a \u0026amp; b) fmt.Printf(\u0026#34;%08b \u0026amp;^%08b = %08b [A \u0026#39;AND NOT\u0026#39; B]\\n\u0026#34;,a,b,a \u0026amp;^ b) fmt.Printf(\u0026#34;%08b\u0026amp;(^%08b)= %08b [A AND (NOT B)]\\n\u0026#34;,a,b,a \u0026amp; (^b)) } 运算符优先级差异 # 级别:初学者 除了「位清除」运算符 (&^) 之外,Go 还有许多其他语言共享的一组标准运算符。但是,运算符优先级并不总是相同。\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Printf(\u0026#34;0x2 \u0026amp; 0x2 + 0x4 -\u0026gt; %#x\\n\u0026#34;,0x2 \u0026amp; 0x2 + 0x4) //prints: 0x2 \u0026amp; 0x2 + 0x4 -\u0026gt; 0x6 //Go: (0x2 \u0026amp; 0x2) + 0x4 //C++: 0x2 \u0026amp; (0x2 + 0x4) -\u0026gt; 0x2 fmt.Printf(\u0026#34;0x2 + 0x2 \u0026lt;\u0026lt; 0x1 -\u0026gt; %#x\\n\u0026#34;,0x2 + 0x2 \u0026lt;\u0026lt; 0x1) //prints: 0x2 + 0x2 \u0026lt;\u0026lt; 0x1 -\u0026gt; 0x6 //Go: 0x2 + (0x2 \u0026lt;\u0026lt; 0x1) //C++: (0x2 + 0x2) \u0026lt;\u0026lt; 0x1 -\u0026gt; 0x8 fmt.Printf(\u0026#34;0xf | 0x2 ^ 0x2 -\u0026gt; %#x\\n\u0026#34;,0xf | 0x2 ^ 0x2) //prints: 0xf | 0x2 ^ 0x2 -\u0026gt; 0xd //Go: (0xf | 0x2) ^ 0x2 //C++: 0xf | (0x2 ^ 0x2) -\u0026gt; 0xf } 未导出的结构字段不进行编码 # 级别:初学者 以小写字母开头的 struct 字段将不被编码 (json、xml、gob 等),因此,当你解码结构时,在这些未导出的字段中最终将得到零值。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; ) type MyData struct { One int two string } func main() { in := MyData{1,\u0026#34;two\u0026#34;} fmt.Printf(\u0026#34;%#v\\n\u0026#34;,in) //prints main.MyData{One:1, two:\u0026#34;two\u0026#34;} encoded,_ := json.Marshal(in) fmt.Println(string(encoded)) //prints {\u0026#34;One\u0026#34;:1} var out MyData json.Unmarshal(encoded,\u0026amp;out) fmt.Printf(\u0026#34;%#v\\n\u0026#34;,out) //prints main.MyData{One:1, two:\u0026#34;\u0026#34;} } 应用退出与活动的 Goroutines # 级别:初学者 应用程序不会等待你的所有 goroutine 完成。对于一般的初学者来说,这是一个常见的错误。每个人都从某个地方开始,所以在犯菜鸟错误时不要觉得丢脸\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { workerCount := 2 for i := 0; i \u0026lt; workerCount; i++ { go doit(i) } time.Sleep(1 * time.Second) fmt.Println(\u0026#34;all done!\u0026#34;) } func doit(workerId int) { fmt.Printf(\u0026#34;[%v] is running\\n\u0026#34;,workerId) time.Sleep(3 * time.Second) fmt.Printf(\u0026#34;[%v] is done\\n\u0026#34;,workerId) } 你会看到的:\n [0] 正在运行\n[1] 正在运行\n全部完成!\n 最常见的解决方案之一是使用 “WaitGroup” 变量。它将允许主 goroutine 等待直到所有工作程序 goroutine 完成。如果你的应用程序具有长时间运行的消息处理循环,则你还需要一种方法向那些 goroutine 发出退出信号的信号。你可以向每个工作人员发送 “杀死” 消息。另一个选择是关闭所有工作人员正在接收的渠道。这是一次发出所有 goroutine 信号的简单方法。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func main() { var wg sync.WaitGroup done := make(chan struct{}) workerCount := 2 for i := 0; i \u0026lt; workerCount; i++ { wg.Add(1) go doit(i,done,wg) } close(done) wg.Wait() fmt.Println(\u0026#34;all done!\u0026#34;) } func doit(workerId int,done \u0026lt;-chan struct{},wg sync.WaitGroup) { fmt.Printf(\u0026#34;[%v] is running\\n\u0026#34;,workerId) defer wg.Done() \u0026lt;- done fmt.Printf(\u0026#34;[%v] is done\\n\u0026#34;,workerId) } 如果你运行此应用,将会看到:\n [0] is running\n[0] is done\n[1] is running\n[1] is done\n 看起来 worker 在主 goroutine 退出之前已经完成。这太棒了!但是 1,你还会看到这样的情况:\n fatal error: all goroutines are asleep - deadlock!\n 这不太好 发生了什么?为什么会出现死锁?当 worker 离开时,它们执行了 wg.Done()。应用程序应该是可以工作的。\n发生死锁是因为每个 Worker 都会获得原始「WaitGroup」变量的副本。当工人执行 wg.Done() 时,它不会影响主 goroutine 中 的「WaitGroup」变量。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func main() { var wg sync.WaitGroup done := make(chan struct{}) wq := make(chan interface{}) workerCount := 2 for i := 0; i \u0026lt; workerCount; i++ { wg.Add(1) go doit(i,wq,done,\u0026amp;wg) } for i := 0; i \u0026lt; workerCount; i++ { wq \u0026lt;- i } close(done) wg.Wait() fmt.Println(\u0026#34;all done!\u0026#34;) } func doit(workerId int, wq \u0026lt;-chan interface{},done \u0026lt;-chan struct{},wg *sync.WaitGroup) { fmt.Printf(\u0026#34;[%v] is running\\n\u0026#34;,workerId) defer wg.Done() for { select { case m := \u0026lt;- wq: fmt.Printf(\u0026#34;[%v] m =\u0026gt; %v\\n\u0026#34;,workerId,m) case \u0026lt;- done: fmt.Printf(\u0026#34;[%v] is done\\n\u0026#34;,workerId) return } } } 现在它可以按预期工作了\n\u0026ldquo;nil\u0026rdquo; 使用 “nil” 通道 # Send and receive operations on a nil channel block forver. It\u0026rsquo;s a well documented behavior, but it can be a surprise for new Go developers.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { var ch chan int for i := 0; i \u0026lt; 3; i++ { go func(idx int) { ch \u0026lt;- (idx + 1) * 2 }(i) } //get first result fmt.Println(\u0026#34;result:\u0026#34;,\u0026lt;-ch) //do other work time.Sleep(2 * time.Second) } 如果运行你的代码,你会看到这样的报错:\nfatal error: all goroutines are asleep - deadlock!\n出现这样的错误是因为你在 select 语句中 case 块中动态启用和禁用了管道。\npackage main import \u0026#34;fmt\u0026#34; import \u0026#34;time\u0026#34; func main() { inch := make(chan int) outch := make(chan int) go func() { var in \u0026lt;- chan int = inch var out chan \u0026lt;- int var val int for { select { case out \u0026lt;- val: out = nil in = inch case val = \u0026lt;- in: out = outch in = nil } } }() go func() { for r := range outch { fmt.Println(\u0026#34;result:\u0026#34;,r) } }() time.Sleep(0) inch \u0026lt;- 1 inch \u0026lt;- 2 time.Sleep(3 * time.Second) } 方法中的接受者不能修改原始值 # 级别:初学者 方法接收者就像常规函数参数一样。如果声明为值,那么你的函数 / 方法将获得接收器参数的副本。这意味着对接收者进行更改不会影响原始值,除非你的接收者是映射或切片变量,并且你要更新集合中的项,或者你要在接收者中更新的字段是指针。\npackage main import \u0026#34;fmt\u0026#34; type data struct { num int key *string items map[string]bool } func (this *data) pmethod() { this.num = 7 } func (this data) vmethod() { this.num = 8 *this.key = \u0026#34;v.key\u0026#34; this.items[\u0026#34;vmethod\u0026#34;] = true } func main() { key := \u0026#34;key.1\u0026#34; d := data{1,\u0026amp;key,make(map[string]bool)} fmt.Printf(\u0026#34;num=%v key=%v items=%v\\n\u0026#34;,d.num,*d.key,d.items) //prints num=1 key=key.1 items=map[] d.pmethod() fmt.Printf(\u0026#34;num=%v key=%v items=%v\\n\u0026#34;,d.num,*d.key,d.items) //prints num=7 key=key.1 items=map[] d.vmethod() fmt.Printf(\u0026#34;num=%v key=%v items=%v\\n\u0026#34;,d.num,*d.key,d.items) //prints num=7 key=v.key items=map[vmethod:true] } "},{"id":29,"href":"/impossible/%E6%96%B0%E6%89%8B%E5%B8%B8%E7%8A%AF%E7%9A%84%E9%94%99%E8%AF%AF/","title":"新手常犯的错误","section":"Impossibles","content":"新手常犯的错误 # 引用: Go 经典译文:50 个 Go 新手易犯的错误(2020版)\n 花括号不能放在单独的一行 # 大多数使用花括号的语言中,你可以选择放置花括号的位置。 但 Go 不一样。 Go 在编译时会自动注入分号,花括号单独一行会导致分号注入错误(无需自己书写分号)。 所以 Go 其实是有分号的\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { // 错误,不能将左大括号放在单独的行上 fmt.Println(\u0026#34;hello there!\u0026#34;) } 编译错误:\n /tmp/sandbox826898458/main.go:6: 语法错误: { 前出现意外的分号或者新的一行\n 正确的写法:\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;works!\u0026#34;) } 未使用的变量 # 如果存在未使用的变量会导致编译失败。但是有一个例外, 只有在函数内部声明的变量未使用才会导致报错,如果你有未使用的全局变量是没问题的,也可以存在未使用的函数参数。\n如果给变量赋值但是没有使用该变量值,你的代码仍将无法编译。你需要以某种方式使用变量值以使编译器通过。\n错误的范例:\npackage main var gvar int //not an error func main() { var one int //error, unused variable two := 2 //error, unused variable var three int //error, even though it\u0026#39;s assigned 3 on the next line three = 3 func(unused string) { fmt.Println(\u0026#34;Unused arg. No compile error\u0026#34;) }(\u0026#34;what?\u0026#34;) } 编译错误:\n /tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used\n 正确的写法:\npackage main import \u0026#34;fmt\u0026#34; func main() { var one int _ = one two := 2 fmt.Println(two) var three int three = 3 one = three var four int four = four } 另一种选择是注释掉或删除未使用的变量\n未使用的导入 # 如果你导入一个包却没有使用它的任何导出函数,接口,结构体或变量,你的代码将会编译失败。\n如果确实需要导入包,你可以使用空白标识符_作为其包名,以避免此编译失败。对于这些副作用,使用空标识符来导入包。\n错误的范例:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) func main() { } 编译错误:\n /tmp/sandbox627475386/main.go:4:导入但未使用:“fmt” /tmp/sandbox627475386/main.go:5:导入但未使用:“ log” /tmp/sandbox627475386/main.go:6:导入但未使用:“time”\n 正确的写法:\npackage main import ( _ \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) var _ = log.Println func main() { _ = time.Now } 另一个选择是删除或注释掉未使用的导入 goimports 工具可以为你提供帮助。\n短变量声明只能在函数内部使用 # 错误的范例:\npackage main myvar := 1 //error func main() { } 编译错误:\n /tmp/sandbox265716165/main.go:3: non-declaration statement outside function body\n 正确的写法:\npackage main var myvar = 1 func main() { } 使用短变量声明重新声明变量 # 你不能在独立的语句中重新声明变量,但在至少声明一个新变量的多变量声明中允许这样做。\n重新声明的变量必须位于同一块中,否则最终将得到隐藏变量。\n错误的范例:\npackage main func main() { one := 0 one := 1 //error } 编译错误:\n /tmp/sandbox706333626/main.go:5: no new variables on left side of :=\n 正确的写法:\npackage main func main() { one := 0 one, two := 1,2 one,two = two,one } 不能使用短变量声明来设置字段值 # 错误的范例:\npackage main import ( \u0026#34;fmt\u0026#34; ) type info struct { result int } func work() (int,error) { return 13,nil } func main() { var data info data.result, err := work() //error fmt.Printf(\u0026#34;info: %+v\\n\u0026#34;,data) } 编译错误:\n prog.go:18: non-name data.result on left side of :=\n 尽管有解决这个问题的办法,但它不太可能改变,因为 Rob Pike 喜欢它「按原样」\n使用临时变量或预先声明所有变量并使用标准赋值运算符。\n正确的写法:\npackage main import ( \u0026#34;fmt\u0026#34; ) type info struct { result int } func work() (int,error) { return 13,nil } func main() { var data info var err error data.result, err = work() //ok if err != nil { fmt.Println(err) return } fmt.Printf(\u0026#34;info: %+v\\n\u0026#34;,data) //prints: info: {result:13} } 偶然的变量隐藏 # 简短的变量声明语法非常方便 (特别是对于那些来自动态语言的变量),以至于可以像对待常规赋值操作一样轻松地对待它。如果你在新的代码块中犯了此错误,将不会有编译器错误,但你的应用程序将无法达到你的期望。\npackage main import \u0026#34;fmt\u0026#34; func main() { x := 1 fmt.Println(x) //prints 1 { fmt.Println(x) //prints 1 x := 2 fmt.Println(x) //prints 2 } fmt.Println(x) //prints 1 (bad if you need 2) } 即使对于有经验的 Go 开发者来说,这也是一个非常常见的陷阱。这很容易出现,可能很难发现。\n你可以使用 vet 命令来查找其中的一些问题。默认情况下,vet 将不执行任何隐藏变量的检查。确保使用 -shadow 标志:go tool vet -shadow your_file.go\n注意,vet 命令不会报告所有的隐藏变量。使用 go-nyet 进行更全面的隐藏变量检查。\n不能使用 「nil」来初始化没有显式类型的变量 # 「nil」标识符可以用作接口,函数,指针,映射,切片和通道的「零值」。如果不指定变量类型,则编译器将无法编译代码,因为它无法猜测类型。\n错误的范例:\npackage main func main() { var x = nil //error _ = x } 编译错误:\n /tmp/sandbox188239583/main.go:4: use of untyped nil\n 正确的写法:\npackage main func main() { var x interface{} = nil _ = x } 使用 「nil」 切片和映射 # 可以将数据添加到「nil」切片中,但是对映射执行相同操作会产生运行时崩溃 (runtime panic)。\n正确的写法:\npackage main func main() { var s []int s = append(s,1) } 错误的范例:\npackage main func main() { var m map[string]int m[\u0026#34;one\u0026#34;] = 1 //error } 映射容量 # 你可以在创建映射时指定映射的容量,但不能在映射中使用 cap() 函数。\n错误的范例:\npackage main func main() { m := make(map[string]int,99) cap(m) //error } 编译错误:\n /tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap\n 字符串不能为「nil」 # 对于习惯于为字符串变量分配「nil」标识符的开发人员来说,这是一个陷阱。\n错误的范例:\npackage main func main() { var x string = nil //error if x == nil { //error x = \u0026#34;default\u0026#34; } } 编译错误:\n /tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)\n 正确的写法:\npackage main func main() { var x string //defaults to \u0026#34;\u0026#34; (zero value) if x == \u0026#34;\u0026#34; { x = \u0026#34;default\u0026#34; } } 数组函数参数 # 如果你是 C 或 C++ 开发者,那么你的数组是指针。当你将数组传递给函数时,这些函数将引用相同的内存位置,因此它们可以更新原始数据。Go 中的数组是值,因此当你将数组传递给函数时,这些函数会获取原始数组数据的副本。如果你尝试更新数组数据,则可能会出现问题。\npackage main import \u0026#34;fmt\u0026#34; func main() { x := [3]int{1,2,3} func(arr [3]int) { arr[0] = 7 fmt.Println(arr) //prints [7 2 3] }(x) fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3]) } 如果你需要更新原始数组数据,请使用数组指针类型。\npackage main import \u0026#34;fmt\u0026#34; func main() { x := [3]int{1,2,3} func(arr *[3]int) { (*arr)[0] = 7 fmt.Println(arr) //prints \u0026amp;[7 2 3] }(\u0026amp;x) fmt.Println(x) //prints [7 2 3] } 另一种选择是使用切片。即使你的函数获得了切片变量的副本,它仍然引用原始数据。\npackage main import \u0026#34;fmt\u0026#34; func main() { x := []int{1,2,3} func(arr []int) { arr[0] = 7 fmt.Println(arr) //prints [7 2 3] }(x) fmt.Println(x) //prints [7 2 3] } 切片和数组「range」子句下的意外值 # 如果你习惯于使用其他语言的「for-in」或 「foreach」语句,则可能发生这种情况。Go 中的「range」子句不同。它生成两个值:第一个值是索引,而第二个值是数据。\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := []string{\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;} for v := range x { fmt.Println(v) //prints 0, 1, 2 } } 正确的写法:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := []string{\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;} for _, v := range x { fmt.Println(v) //prints a, b, c } } 切片和数组是一维的 # Go 看起来它支持多维数组和切片,但它并不支持。创建数组的数组或切片的切片是可能的。对于依赖于动态多维数组的数值计算应用程序来说,在性能和复杂性方面远远不够理想。\n你可以使用原始的一维数组,「独立」切片的切片以及「共享数据」切片的切片来构建动态多维数组。\n如果使用的是原始一维数组,则需要在数组增长时负责索引,边界检查和内存重新分配。\n使用「独立」切片的切片创建动态多维数组是一个两步过程。首先,你必须创建外部切片。然后,你必须分配每个内部切片。内部切片彼此独立。你可以扩大和缩小它们,而不会影响其他内部切片。\npackage main func main() { x := 2 y := 4 table := make([][]int,x) for i:= range table { table[i] = make([]int,y) } } 使用 「共享数据」切片的切片创建动态多维数组是一个三步过程。首先,你必须创建保存原始数据的数据「容器」切片。然后,创建外部切片。最后,通过重新排列原始数据切片来初始化每个内部切片。\npackage main import \u0026#34;fmt\u0026#34; func main() { h, w := 2, 4 raw := make([]int,h*w) for i := range raw { raw[i] = i } fmt.Println(raw,\u0026amp;raw[4]) //prints: [0 1 2 3 4 5 6 7] \u0026lt;ptr_addr_x\u0026gt; table := make([][]int,h) for i:= range table { table[i] = raw[i*w:i*w + w] } fmt.Println(table,\u0026amp;table[1][0]) //prints: [[0 1 2 3] [4 5 6 7]] \u0026lt;ptr_addr_x\u0026gt; } 对于多维数组和切片有一个规范 / 建议,但目前看来这是低优先级的功能。\n访问不存在的映射键 # 对于希望获得「nil」标识符的开发人员来说这是一个陷阱 (就像其他语言一样)。如果相应数据类型的「零值」为「 nil」,则返回值将为「 nil」,但对于其他数据类型,返回值将不同。检查适当的「零值」可用于确定映射记录是否存在,但是并不总是可靠的 (例如,如果你的布尔值映射中「零值」为 false,你会怎么做)。知道给定映射记录是否存在的最可靠方法是检查由映射访问操作返回的第二个值。\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := map[string]string{\u0026#34;one\u0026#34;:\u0026#34;a\u0026#34;,\u0026#34;two\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;three\u0026#34;:\u0026#34;c\u0026#34;} if v := x[\u0026#34;two\u0026#34;]; v == \u0026#34;\u0026#34; { //incorrect fmt.Println(\u0026#34;no entry\u0026#34;) } } 正确的写法:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := map[string]string{\u0026#34;one\u0026#34;:\u0026#34;a\u0026#34;,\u0026#34;two\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;three\u0026#34;:\u0026#34;c\u0026#34;} if _,ok := x[\u0026#34;two\u0026#34;]; !ok { fmt.Println(\u0026#34;no entry\u0026#34;) } } 字符串是不可变的 # 尝试使用索引运算符更新字符串变量中的单个字符将导致失败。字符串是只读字节片 (具有一些其他属性)。如果确实需要更新字符串,则在必要时使用字节片而不是将其转换为字符串类型。\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := \u0026#34;text\u0026#34; x[0] = \u0026#39;T\u0026#39; fmt.Println(x) } 编译错误:\n /tmp/sandbox305565531/main.go:7: cannot assign to x[0]\n 正确的用法:\npackage main import \u0026#34;fmt\u0026#34; func main() { x := \u0026#34;text\u0026#34; xbytes := []byte(x) xbytes[0] = \u0026#39;T\u0026#39; fmt.Println(string(xbytes)) //prints Text } 请注意,这不是真正更新文本字符串中字符的正确方法,因为给定字符可以存储在多个字节中。如果确实需要更新文本字符串,请先将其转换为符文切片。即使使用符文切片,单个字符也可能跨越多个符文。例如,如果你的字符带有重音符号,则可能会发生这种情况。「字符」的这种复杂和模凌两可的性质是将 Go 字符串表示为字节序列的原因。\n字符串和字节片之间的转换 # 当你将字符串转换为字节片 (反之亦然) 时,你将获得原始数据的完整副本。这不像其他语言中的强制转换操作,也不像在新切片变量指向原始字节片所使用的相同基础数组的切片一样。\nGo 对于 []byte 转 string ,和 string 转 []byte 确实做了一些优化,以免转换额外分配 (在待办事项列表中还对此进行了更多的优化)\n第一个优化避免了在 map[string] 获取 m[string(key)] 中使用 []byte 的 keys 查找条目时的额外分配。\n第二个优化避免了在 for range 字符串被转换的语句 []byte: for i,v := range []byte(str) {...}.\n字符串并不总是 UTF8 文本 # 等级:新手 字符串的值不一定是 UTF8 文本。它们可以包含任意字节。只有在使用字符串字面值时,字符串才是 UTF8。即使这样,它们也可以使用转译序列包括其他数据。若要了解你是否具有 UTF8 文本字符串,请使用 「unicode/uft8」包中的函数 ValidString()。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;unicode/utf8\u0026#34; ) func main() { data1 := \u0026#34;ABC\u0026#34; fmt.Println(utf8.ValidString(data1)) //prints: true data2 := \u0026#34;A\\xfeC\u0026#34; fmt.Println(utf8.ValidString(data2)) //prints: false } 字符串长度 # 等级:新手 假设你是 python 开发者,并且使用下面的代码:\ndata = u\u0026#39;♥\u0026#39; print(len(data)) #prints: 1 当你将其转换为类似的 Go 代码时,你可能会感到惊讶。\npackage main import \u0026#34;fmt\u0026#34; func main() { data := \u0026#34;♥\u0026#34; fmt.Println(len(data)) //prints: 3 } 内置的 len() 函数返回字节数而不是字符数,就像 Python 中对 unicode 字符串所做的那样。\n要在 Go 中获得相同的结果,请使用 「unicode/utf8」包中的 RuneCountInString() 函数。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;unicode/utf8\u0026#34; ) func main() { data := \u0026#34;♥\u0026#34; fmt.Println(utf8.RuneCountInString(data)) //prints: 1 从技术上讲, RuneCountInString() 函数不会返回字符数,因为单个字符可能跨越多个符文。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;unicode/utf8\u0026#34; ) func main() { data := \u0026#34;é\u0026#34; fmt.Println(len(data)) //prints: 3 fmt.Println(utf8.RuneCountInString(data)) //prints: 2 } 在多行切片,数组和映射字面值中缺少逗号 # 等级:新手 错误的范例:\npackage main func main() { x := []int{ 1, 2 //error } _ = x } 编译错误:\n /tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }\n 正确的写法:\npackage main func main() { x := []int{ 1, 2, } x = x y := []int{3,4,} //no error y = y } 如果在声明折叠为一行时留下逗号,则不会出现编译错误。\nlog.Fatal 与 log.Panic 比 Log 要做的更多 # 级别:新手 日志库通常提供不同的日志级别。与那些日志记录库不同,Go 中的日志包的作用远不止于日志记录。如果在你的应用中调用 Go 的 Fatal *() 和 Panic *() 函数,Go 将会终止你的应用\npackage main import \u0026#34;log\u0026#34; func main() { log.Fatalln(\u0026#34;Fatal Level: log entry\u0026#34;) //app exits here log.Println(\u0026#34;Normal Level: log entry\u0026#34;) } 内置数据结构操作不同步 # 等级:新手 尽管 Go 有很多支持并发的原生特性,但是并发安全的数据集合不在这些特性中。开发者需要保证对这些数据集合的并发更新操作是原子性的,比如对 map 的并发更新。Go 推荐使用 channels 来实现对集合数据的原子性操作。当然如果「sync」包更适合你的应用也可以利用「sync」包来实现。\n「range」语句对于字符串的操作 # 等级:新手 「range」语句的第一个返回值是当前「字符」(该字符可能是 unicode 码点 /rune)的第一个字节在字符串中按字节的索引值(unicode 是多字节编码),「range」语句的第二个返回值是当前的「字符」。这是 Go 其他语言不同的地方,其他语言的迭代操作大多是返回当前字符的位置,但 Go「range」返回的并不是当前字符的位置。在实际的使用中一个字符可能是由多个 rune 表示的,所以当我们需要处理字符时强烈推荐使用「norm」包(golang.org/x/text/unicode/norm)。\n带有字符串变量的 for range 子句将尝试把数据解释为 UTF8 文本。对于任何它无法理解的字节序列,它将返回 0xfffd runes (即 Unicode 替换字符),而不是实际数据。如果你在字符串变量中存储了任意 (非 UTF8 文本) 数据,请确保将其转换为字节切片,以按原样获取所有存储的数据。\npackage main import \u0026#34;fmt\u0026#34; func main() { data := \u0026#34;A\\xfe\\x02\\xff\\x04\u0026#34; for _,v := range data { fmt.Printf(\u0026#34;%#x \u0026#34;,v) } //prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok) fmt.Println() for _,v := range []byte(data) { fmt.Printf(\u0026#34;%#x \u0026#34;,v) } //prints: 0x41 0xfe 0x2 0xff 0x4 (good) } switch 语句中的 Fallthrough 行为 # 级别:新手 在 \u0026ldquo;switch\u0026rdquo; 语句中的 \u0026ldquo;case\u0026rdquo; 块,其缺省行为是 break 出 \u0026ldquo;switch\u0026rdquo;。这一行为与其它语言不同,其它语言的缺省行为是,继续执行下一个 \u0026ldquo;case\u0026rdquo; 块。\npackage main import \u0026#34;fmt\u0026#34; func main() { isSpace := func(ch byte) bool { switch(ch) { case \u0026#39; \u0026#39;: //error case \u0026#39;\\t\u0026#39;: return true } return false } fmt.Println(isSpace(\u0026#39;\\t\u0026#39;)) //prints true (ok) fmt.Println(isSpace(\u0026#39; \u0026#39;)) //prints false (not ok) } 你可以通过在每个 \u0026ldquo;case\u0026rdquo; 块的最后加入 \u0026ldquo;fallthrough\u0026rdquo; 语句来迫使 \u0026ldquo;case\u0026rdquo; 块继续往下执行。你也可以重写你的 \u0026ldquo;switch\u0026rdquo; 语句,在 \u0026ldquo;case\u0026rdquo; 块中使用表达式列表来达到这一目的。\npackage main import \u0026#34;fmt\u0026#34; func main() { isSpace := func(ch byte) bool { switch(ch) { case \u0026#39; \u0026#39;, \u0026#39;\\t\u0026#39;: return true } return false } fmt.Println(isSpace(\u0026#39;\\t\u0026#39;)) //prints true (ok) fmt.Println(isSpace(\u0026#39; \u0026#39;)) //prints true (ok) } 发送到无缓冲通道的消息在目标接收器准备就绪后立即返回 # 等级:新手 直到接收方处理完你的消息后,发送才会被阻止。根据运行代码的机器,接收方 goroutine 可能会或可能没有足够的时间在发送方继续执行之前处理消息。\npackage main import \u0026#34;fmt\u0026#34; func main() { ch := make(chan string) go func() { for m := range ch { fmt.Println(\u0026#34;processed:\u0026#34;,m) } }() ch \u0026lt;- \u0026#34;cmd.1\u0026#34; ch \u0026lt;- \u0026#34;cmd.2\u0026#34; //won\u0026#39;t be processed } 发送到关闭通道会引起崩溃 # 等级:新手 从关闭的通道接收是安全的。接收语句中的 ok 返回值将设置为 false 表示未接收到任何数据。如果你是从缓冲通道接收到的数据,则将首先获取缓冲数据,一旦缓冲数据为空,返回的 ok 返回值将为 false。\n发送数据到一个已经关闭的 channel 会触发 panic。 这是一个不容争论的事实,但是对于一个 Go 开发新手来说这样的事实可能不太容易理解,可能会更期望发送行为像接收行为那样。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { ch := make(chan int) for i := 0; i \u0026lt; 3; i++ { go func(idx int) { ch \u0026lt;- (idx + 1) * 2 }(i) } //获取第一个结果 fmt.Println(\u0026lt;-ch) close(ch) //这样做很不好 (因为在协程中还有动作在向 channel 发送数据) //做些其他的事情 time.Sleep(2 * time.Second) } 根据你的应用程序,修复这样的程序将会有所不同。修改细微的代码不让 panic 中断程序是次要的,因为可能你更加需要修改程序的逻辑设计。无论哪种方式,你都需要确保你的应用程序不会在 channel 已经关闭的情况下发送数据给它。\n可以通过使用特殊的取消渠道来通知剩余的工作人员不再需要他们的结果,从而解决该示例问题。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { ch := make(chan int) done := make(chan struct{}) for i := 0; i \u0026lt; 3; i++ { go func(idx int) { select { case ch \u0026lt;- (idx + 1) * 2: fmt.Println(idx,\u0026#34;sent result\u0026#34;) case \u0026lt;- done: fmt.Println(idx,\u0026#34;exiting\u0026#34;) } }(i) } //get first result fmt.Println(\u0026#34;result:\u0026#34;,\u0026lt;-ch) close(done) //do other work time.Sleep(3 * time.Second) } "},{"id":30,"href":"/impossible/%E8%BF%9B%E9%98%B6%E5%B8%B8%E7%8A%AF%E7%9A%84%E9%94%99%E8%AF%AF/","title":"进阶常犯的错误","section":"Impossibles","content":"进阶常犯的错误 # 引用: Go 经典译文:50 个 Go 新手易犯的错误(2020版)\n 关闭 HTTP 响应 Body # 级别:中级 当使用 net/http 库发送 http 请求时,会返回一个 *http.Respose 变量。 如果你不读取响应 Body,依然需要关闭这个 Body。 注意对于空 Body 也必须关闭。 对于 GO 程序员新手很容易忘记这点。\n一些 GO 程序员新手尝试关闭响应 Body,但他们在错误的位置进行了关闭 Body。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { resp, err := http.Get(\u0026#34;https://api.ipify.org?format=json\u0026#34;) defer resp.Body.Close()//错误的方法 if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } 这种方法适合请求成功的情况,但是如果 http 请求失败,则 resp 变量可能为 nil,这将导致运行触发 panic。\n关闭 http 响应 Body 的最常见方法,应该是在 http 响应检查错误之后使用 defer 调用 Close 方法。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { resp, err := http.Get(\u0026#34;https://api.ipify.org?format=json\u0026#34;) if err != nil { fmt.Println(err) return } defer resp.Body.Close()//ok, most of the time :-) body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } 在大多数情况下,当 http 请求失败时,resp 变量将为 nil,而 err 变量将为非空。 但是当重定向失败时,两个变量都将为非空。 这意味着 Body 仍然可能会未关闭而导致泄漏。\n你可以通过在 http 响应错误处理时,添加一段关闭非空响应 Body 的代码这解决这个问题 (重定向时响应和 err 都是非空,检查了 err 返回错误而没有关闭 Body), 使用一个 defer 关闭所有失败和成功请求的响应 Body。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { resp, err := http.Get(\u0026#34;https://api.ipify.org?format=json\u0026#34;) if resp != nil { defer resp.Body.Close() } if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } resp.Body.Close() 方法的底层实现是读取并丢弃响应 Body 的剩余数据。 这样可以保证使用了 keepalive http 长连接机制,可以将 http 连接复用,用来发送另外一个请求。 在最新的 http 客户端处理方法是不同的。 但是现在你需要读取并丢弃其余的响应数据。 如果你不读取并丢弃剩余数据,那么 http 连接可能会关闭而不是被长连接复用。 这个小陷阱应该记录在 Go 1.5 中。\n如果复用 http 长连接对于你的程序很重要,那么可能需要在响应处理逻辑的末尾添加以下内容:\n_, err = io.Copy(ioutil.Discard, resp.Body) 如果你没有读取全部响应 Body,则需要这样丢弃数据,如果使用以下代码处理 json API 响应,json 库只读取了部分 Body 就完成了 json 对象解析,未读取完毕 Body,则可能会发生这种情况:\njson.NewDecoder(resp.Body).Decode(\u0026amp;data) 关闭 HTTP 连接 # 级别:中级 某些 HTTP 服务器会打开长连接(基于 HTTP/1.1 规范和服务器的 Keepalive 机制)。 在默认情况下,net/http 库客户端在收到 HTTP 服务端要求关闭时,才会关闭长连接。 这意味着程序在某些情况下没有关闭长连接,可能会泄露系统 fd,用完操作系统的套接字 / 文件描述符。\n你可以在请求发送前将 *http.Requsst 对象的 Close 字段设置为 true, 用于关闭 net/http 库客户端连接。\n另一种方法是添加 Connection Header 并设置值为 close。目标 HTTP 服务器响应也应该返回 Header Connection:close。当 net/http 库客户端看到这个 Header 时,它也会关闭连接。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { req, err := http.NewRequest(\u0026#34;GET\u0026#34;,\u0026#34;http://golang.org\u0026#34;,nil) if err != nil { fmt.Println(err) return } req.Close = true // 或者使用下面的这行方法: //req.Header.Add(\u0026#34;Connection\u0026#34;, \u0026#34;close\u0026#34;) resp, err := http.DefaultClient.Do(req) if resp != nil { defer resp.Body.Close() } if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(len(string(body))) } 你还可以在全局范围内禁用使用 HTTP 长连接 (KeepAlives),创建一个自定义使用的 *http.Transport 对象,用于发送 http 客户端的请求。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { tr := \u0026amp;http.Transport{DisableKeepAlives: true} client := \u0026amp;http.Client{Transport: tr} resp, err := client.Get(\u0026#34;http://golang.org\u0026#34;) if resp != nil { defer resp.Body.Close() } if err != nil { fmt.Println(err) return } fmt.Println(resp.StatusCode) body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(len(string(body))) } 如果你同时向一个 http 服务器发送大量请求,则可以打开 KeepAlives 选项使用长连接。但是如果你在应用是短时间内,向不同的 HTTP 服务器发送一两个请求 (少量请求),那么则最好在收到 http 响应后立刻关闭网络连,设置更大的操作系统打开文件句柄数量是一个好方法 (ulimit -n)。正确的解决方法取决于你的应用程序。\nJSON 编码器添加换行符 # 级别:中级 你发现你为 JSON 编码功能编写的测试由于未获得期望值而导致测试失败,为什么会这样?如果你是用的是 JSON 编码器对象,则在编码的 JSON 对象的末尾将获得一个额外的换行符。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; ) func main() { data := map[string]int{\u0026#34;key\u0026#34;: 1} var b bytes.Buffer json.NewEncoder(\u0026amp;b).Encode(data) raw,_ := json.Marshal(data) if b.String() == string(raw) { fmt.Println(\u0026#34;same encoded data\u0026#34;) } else { fmt.Printf(\u0026#34;\u0026#39;%s\u0026#39; != \u0026#39;%s\u0026#39;\\n\u0026#34;,raw,b.String()) //prints: //\u0026#39;{\u0026#34;key\u0026#34;:1}\u0026#39; != \u0026#39;{\u0026#34;key\u0026#34;:1}\\n\u0026#39; } } JSON 编码器对象旨在用于流传输。使用 JSON 进行流传输通常意味着用换行符分隔的 JSON 对象,这就是为什么 Encode 方法添加换行符的原因。这是正常的行为,但是通常被忽略或遗忘。\nJSON 包在键和字符串值中转义特殊的 HTML 字符 # 级别:中级 这是已记录的行为,但是你必须仔细阅读所有 JSON 包文档以了解有关情况。SetEscapeHTML 方法描述讨论了 and 字符 (小于和大于) 的默认编码行为。\n由于许多原因,这是 Go 团队非常不幸的设计决定。首先,你不能为 json.Marshal 调用禁用此行为。其次,这是一个实施不当的安全功能,因为它假定执行 HTML 编码足以防止所有 Web 应用程序中的 XSS 漏洞。在许多可以使用数据的上下文中,每个上下文需要自己的编码方法。最后,这很糟糕,因为它假定 JSON 的主要用例是网页,默认情况下会破坏配置库和 REST / HTTP API。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; ) func main() { data := \u0026#34;x \u0026lt; y\u0026#34; raw,_ := json.Marshal(data) fmt.Println(string(raw)) //prints: \u0026#34;x \\u003c y\u0026#34; \u0026lt;- probably not what you expected var b1 bytes.Buffer json.NewEncoder(\u0026amp;b1).Encode(data) fmt.Println(b1.String()) //prints: \u0026#34;x \\u003c y\u0026#34; \u0026lt;- probably not what you expected var b2 bytes.Buffer enc := json.NewEncoder(\u0026amp;b2) enc.SetEscapeHTML(false) enc.Encode(data) fmt.Println(b2.String()) //prints: \u0026#34;x \u0026lt; y\u0026#34; \u0026lt;- looks better } 给 Go 团队的建议\u0026hellip; 选择加入。\n将 JSON 数字解组为接口值 # 级别:中级 默认情况下,当你将 JSON 数据解码 / 解组到接口中时,Go 会将 JSON 中的数字值视为 float64 数字。这意味着以下代码将因失败而失败:\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data = []byte(`{\u0026#34;status\u0026#34;: 200}`) var result map[string]interface{} if err := json.Unmarshal(data, \u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } var status = result[\u0026#34;status\u0026#34;].(int) //error fmt.Println(\u0026#34;status value:\u0026#34;,status) } 运行时 Panic:\n panic: 接口转换:接口是 float64,而不是 int\n 如果你尝试解码的 JSON 值为整数,则可以使用服务器选项。\n选项一:按原样使用 float 值\n选项二:将浮点值转换为所需的整数类型。\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data = []byte(`{\u0026#34;status\u0026#34;: 200}`) var result map[string]interface{} if err := json.Unmarshal(data, \u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } var status = uint64(result[\u0026#34;status\u0026#34;].(float64)) //ok fmt.Println(\u0026#34;status value:\u0026#34;,status) } 选项三:使用 Decoder 类型解组 JSON,并使用 Number 接口类型告诉它表示 JSON 数字。\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data = []byte(`{\u0026#34;status\u0026#34;: 200}`) var result map[string]interface{} var decoder = json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(\u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } var status,_ = result[\u0026#34;status\u0026#34;].(json.Number).Int64() //ok fmt.Println(\u0026#34;status value:\u0026#34;,status) } 你可以使用 Number 值的字符串表示形式将其解组为其他数字类型:\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data = []byte(`{\u0026#34;status\u0026#34;: 200}`) var result map[string]interface{} var decoder = json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(\u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } var status uint64 if err := json.Unmarshal([]byte(result[\u0026#34;status\u0026#34;].(json.Number).String()), \u0026amp;status); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } fmt.Println(\u0026#34;status value:\u0026#34;,status) } 选项四:使用 struct 类型将你的数字值映射到所需的数字类型。\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data = []byte(`{\u0026#34;status\u0026#34;: 200}`) var result struct { Status uint64 `json:\u0026#34;status\u0026#34;` } if err := json.NewDecoder(bytes.NewReader(data)).Decode(\u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } fmt.Printf(\u0026#34;result =\u0026gt; %+v\u0026#34;,result) //prints: result =\u0026gt; {Status:200} } 选项五:使用 struct 将你的数值映射到 json.RawMessage 类型,如果你需要延迟值解码。\n如果你必须执行条件 JSON 字段解码 (其中字段类型或结构可能会更改),则此选项很有用。\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { records := [][]byte{ []byte(`{\u0026#34;status\u0026#34;: 200, \u0026#34;tag\u0026#34;:\u0026#34;one\u0026#34;}`), []byte(`{\u0026#34;status\u0026#34;:\u0026#34;ok\u0026#34;, \u0026#34;tag\u0026#34;:\u0026#34;two\u0026#34;}`), } for idx, record := range records { var result struct { StatusCode uint64 StatusName string Status json.RawMessage `json:\u0026#34;status\u0026#34;` Tag string `json:\u0026#34;tag\u0026#34;` } if err := json.NewDecoder(bytes.NewReader(record)).Decode(\u0026amp;result); err != nil { fmt.Println(\u0026#34;error:\u0026#34;, err) return } var sstatus string if err := json.Unmarshal(result.Status, \u0026amp;sstatus); err == nil { result.StatusName = sstatus } var nstatus uint64 if err := json.Unmarshal(result.Status, \u0026amp;nstatus); err == nil { result.StatusCode = nstatus } fmt.Printf(\u0026#34;[%v] result =\u0026gt; %+v\\n\u0026#34;,idx,result) } } 十六进制或其他非 UTF8JSON 字符串转义的值不正确 # 级别:中等 Go 默认使用的字符串编码是 UTF8 编码的。这意味着你不能在 JSON 字符串中使用任意十六进制转义成的二进制数据(并且还必须转义反斜杠)。这确实是 Go 继承的 JSON 不足,但是在 Go 应用程序中经常发生,因此无论如何都要提一下。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; ) type config struct { Data string `json:\u0026#34;data\u0026#34;` } func main() { raw := []byte(`{\u0026#34;data\u0026#34;:\u0026#34;\\xc2\u0026#34;}`) var decoded config if err := json.Unmarshal(raw, \u0026amp;decoded); err != nil { fmt.Println(err) // 输出:字符串转义中的无效字符\u0026#39;x\u0026#39; } } 如果 Go 尝试序列化一个十六进制字符串,则 Unmarshal/Decode 方法调用将失败。如果需要在字符串中使用十六进制字符,需要使用反斜杠转义,并确保使用另一个反斜杠转义反斜杠。如果要使用十六进制编码的二进制数据,可以转义反斜杠,然后使用 JSON 字符串中的解码的数据进行十六进制编码。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; ) type config struct { Data string `json:\u0026#34;data\u0026#34;` } func main() { raw := []byte(`{\u0026#34;data\u0026#34;:\u0026#34;\\\\xc2\u0026#34;}`) var decoded config json.Unmarshal(raw, \u0026amp;decoded) fmt.Printf(\u0026#34;%#v\u0026#34;,decoded) //prints: main.config{Data:\u0026#34;\\\\xc2\u0026#34;} //todo: 对已解码的数据进行十六进制转义解码 } 另一种方法是在 JSON 对象中使用字节数组 / 切片数据类型,但是二进制数据将必须使用 base64 编码。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;encoding/json\u0026#34; ) type config struct { Data []byte `json:\u0026#34;data\u0026#34;` } func main() { raw := []byte(`{\u0026#34;data\u0026#34;:\u0026#34;wg==\u0026#34;}`) var decoded config if err := json.Unmarshal(raw, \u0026amp;decoded); err != nil { fmt.Println(err) } fmt.Printf(\u0026#34;%#v\u0026#34;,decoded) //prints: main.config{Data:[]uint8{0xc2}} } 其他需要注意的是 Unicode 替换字符(U+FFFD)。 Go 将使用替换字符代替无效的 UTF8,因此 Unmarshal/Decode 调用不会失败,但是你获得的字符串可能不是你需要的结果。\n比较结构体 / 数组 / 切片 / Map # 级别:中级 如果结构体的每个字段都具有可比性 , 那么则可以使用等号运算符 == 比较结构体变量。\npackage main import \u0026#34;fmt\u0026#34; type data struct { num int fp float32 complex complex64 str string char rune yes bool events \u0026lt;-chan string handler interface{} ref *byte raw [10]byte } func main() { v1 := data{} v2 := data{} fmt.Println(\u0026#34;v1 == v2:\u0026#34;,v1 == v2) //prints: v1 == v2: true } 如果结构体的任意一个属性不具有可比性,那么使用等号运算符在编译时就会显示报错。注意,数组的数据类型具有可比性时,数组才能比较。\npackage main import \u0026#34;fmt\u0026#34; type data struct { num int //ok checks [10]func() bool //无法比较 doit func() bool //无法比较 m map[string] string //无法比较 bytes []byte //无法比较 } func main() { v1 := data{} v2 := data{} fmt.Println(\u0026#34;v1 == v2:\u0026#34;,v1 == v2) } GO 提供了一些辅助函数用来比较无法比较的变量。\n最常见的方法就是使用反射库的 DeepEqual() 函数。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) type data struct { num int //ok checks [10]func() bool //无法比较 doit func() bool //无法比较 m map[string] string //无法比较 bytes []byte //无法比较 } func main() { v1 := data{} v2 := data{} fmt.Println(\u0026#34;v1 == v2:\u0026#34;,reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true m1 := map[string]string{\u0026#34;one\u0026#34;: \u0026#34;a\u0026#34;,\u0026#34;two\u0026#34;: \u0026#34;b\u0026#34;} m2 := map[string]string{\u0026#34;two\u0026#34;: \u0026#34;b\u0026#34;, \u0026#34;one\u0026#34;: \u0026#34;a\u0026#34;} fmt.Println(\u0026#34;m1 == m2:\u0026#34;,reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true s1 := []int{1, 2, 3} s2 := []int{1, 2, 3} fmt.Println(\u0026#34;s1 == s2:\u0026#34;,reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true } 除了运行缓慢 (可能对你的应用程序造成破坏或可能不会破坏交易) 之外,DeepEqual() 也有自己的陷阱。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) func main() { var b1 []byte = nil b2 := []byte{} fmt.Println(\u0026#34;b1 == b2:\u0026#34;,reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false } DeepEqual() 认为空切片不等于 “nil” 切片。此行为与你使用 bytes.Equal() 函数获得的行为不同。bytes.Equal() 认为 “nil” 和空片相等。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;bytes\u0026#34; ) func main() { var b1 []byte = nil b2 := []byte{} fmt.Println(\u0026#34;b1 == b2:\u0026#34;,bytes.Equal(b1, b2)) //prints: b1 == b2: true } DeepEqual() 比较切片并不总是完美的。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;encoding/json\u0026#34; ) func main() { var str string = \u0026#34;one\u0026#34; var in interface{} = \u0026#34;one\u0026#34; fmt.Println(\u0026#34;str == in:\u0026#34;,str == in,reflect.DeepEqual(str, in)) //prints: str == in: true true v1 := []string{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;} v2 := []interface{}{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;} fmt.Println(\u0026#34;v1 == v2:\u0026#34;,reflect.DeepEqual(v1, v2)) //prints: v1 == v2: false (not ok) data := map[string]interface{}{ \u0026#34;code\u0026#34;: 200, \u0026#34;value\u0026#34;: []string{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;}, } encoded, _ := json.Marshal(data) var decoded map[string]interface{} json.Unmarshal(encoded, \u0026amp;decoded) fmt.Println(\u0026#34;data == decoded:\u0026#34;,reflect.DeepEqual(data, decoded)) //prints: data == decoded: false (not ok) } 如果你的 []byte(或字符串) 包含文本数据,当你需要使用不区分大小写比较值时,你可能倾向于使用使用 \u0026ldquo;bytes\u0026rdquo; 和 \u0026ldquo;string\u0026rdquo; 库的 ToUpper()/ToLower() 函数 (在使用 ==,bytes.Equal() 或 bytes.Compare() 比较之前)。\n这种方法适合英文,但是却不适合许多其他语言的文本。正确的方法应该使用 strings.EqualFold() 和 bytes.EqualFold() 方法进行比较。\n如果你的 []byte 中包含了验证用户信息的机密信息(例如,加密哈希,令牌等), 请不要使用 reflect.DeepEqual() 或 bytes.Equal() 或 bytes.Compare() 函数。因为这些函数可能是你受到 定时攻击,为了比较泄露时间信息,请使用 \u0026lsquo;crypto/subtle\u0026rsquo; 库 (例如:subtle.ConstantTimeCompare())。\n从 Panic 中恢复 # 级别:中等 recover() 函数可用于捕获 / 拦截 panic。 但是只有在 defer 函数中,调用 recover() 才能达到目的。\n不正确:\npackage main import \u0026#34;fmt\u0026#34; func main() { recover() // 什么也没执行 panic(\u0026#34;not good\u0026#34;) recover() // 不会执行到 :) fmt.Println(\u0026#34;ok\u0026#34;) } 生效:\npackage main import \u0026#34;fmt\u0026#34; func main() { defer func() { fmt.Println(\u0026#34;recovered:\u0026#34;,recover()) }() panic(\u0026#34;not good\u0026#34;) } 仅在你的 defer 函数中直接调用 recover() 时才有效。\n失败:\npackage main import \u0026#34;fmt\u0026#34; func doRecover() { fmt.Println(\u0026#34;recovered =\u0026gt;\u0026#34;,recover()) //prints: recovered =\u0026gt; \u0026lt;nil\u0026gt; } func main() { defer func() { doRecover() //panic is not recovered }() panic(\u0026#34;not good\u0026#34;) } 使用或更新切片 / 数组 / Map Rnage 遍历的数据 # 级别:中等 在 \u0026ldquo;Range\u0026rdquo; 范围的产生是数据是集合的元素副本,这些值不是原始数据的引用,这意味修改 Range 的值不会改变原始数据。这也意味获得的值地址也不会提供执行原始数据的指针。\npackage main import \u0026#34;fmt\u0026#34; func main() { data := []int{1,2,3} for _,v := range data { v *= 10 //原始项目不变 } fmt.Println(\u0026#34;data:\u0026#34;,data) //prints data: [1 2 3] } 如果需要修改原始数据,需要使用索引访问数据。\npackage main import \u0026#34;fmt\u0026#34; func main() { data := []int{1,2,3} for i,_ := range data { data[i] *= 10 } fmt.Println(\u0026#34;data:\u0026#34;,data) //prints data: [10 20 30] } 如果你的集合包含指针类型,那么规则有些不同。如果希望原始数据指向另外一个值,则仍然需要使用索引操作,但是也可以使用 \u0026ldquo;for range\u0026rdquo; 语法中第二个值来更新存储在目标的数据。\npackage main import \u0026#34;fmt\u0026#34; func main() { data := []*struct{num int} {{1},{2},{3}} for _,v := range data { v.num *= 10 } fmt.Println(data[0],data[1],data[2]) //prints \u0026amp;{10} \u0026amp;{20} \u0026amp;{30} } 切片的隐藏数据 # 级别:中级 重新分割切片时,新切片将引用旧切片的底层数组。如果你忘记这个行为,并且分配相对较大切片,则从中创建了新建的切片引用了部分原始数据,则可能导致意外的底层数据使用。\npackage main import \u0026#34;fmt\u0026#34; func get() []byte { raw := make([]byte,10000) fmt.Println(len(raw),cap(raw),\u0026amp;raw[0]) //prints: 10000 10000 \u0026lt;byte_addr_x\u0026gt; return raw[:3] } func main() { data := get() fmt.Println(len(data),cap(data),\u0026amp;data[0]) //prints: 3 10000 \u0026lt;byte_addr_x\u0026gt; } 为避免此陷阱,请确保从临时切片中复制所需的数据(而不是切割切片)。\npackage main import \u0026#34;fmt\u0026#34; func get() []byte { raw := make([]byte,10000) fmt.Println(len(raw),cap(raw),\u0026amp;raw[0]) //prints: 10000 10000 \u0026lt;byte_addr_x\u0026gt; res := make([]byte,3) copy(res,raw[:3]) return res } func main() { data := get() fmt.Println(len(data),cap(data),\u0026amp;data[0]) //prints: 3 3 \u0026lt;byte_addr_y\u0026gt; } 切片数据污染 # 等级:中级 假如需要修改路径 (存储在切片中)。你可以重新设置路径用来引用每个目录,从而修改第一个目录的名称,然后将这些名称合并创建新路径。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;bytes\u0026#34; ) func main() { path := []byte(\u0026#34;AAAA/BBBBBBBBB\u0026#34;) sepIndex := bytes.IndexByte(path,\u0026#39;/\u0026#39;) dir1 := path[:sepIndex] dir2 := path[sepIndex+1:] fmt.Println(\u0026#34;dir1 =\u0026gt;\u0026#34;,string(dir1)) //prints: dir1 =\u0026gt; AAAA fmt.Println(\u0026#34;dir2 =\u0026gt;\u0026#34;,string(dir2)) //prints: dir2 =\u0026gt; BBBBBBBBB dir1 = append(dir1,\u0026#34;suffix\u0026#34;...) path = bytes.Join([][]byte{dir1,dir2},[]byte{\u0026#39;/\u0026#39;}) fmt.Println(\u0026#34;dir1 =\u0026gt;\u0026#34;,string(dir1)) //prints: dir1 =\u0026gt; AAAAsuffix fmt.Println(\u0026#34;dir2 =\u0026gt;\u0026#34;,string(dir2)) //prints: dir2 =\u0026gt; uffixBBBB (not ok) fmt.Println(\u0026#34;new path =\u0026gt;\u0026#34;,string(path)) } 结果并不是预料的 \u0026ldquo;AAAAsuffix/BBBBBBBBB\u0026rdquo; 这样,而是 \u0026ldquo;AAAAsuffix/uffixBBBB\u0026rdquo;。发送这种请求是因为两个路径切片的引用了相同的原始底层数据。这意味修改原始路径也会被修改。根据你的程序情况,这也可能会是一个问题。\n可以通过分配新的切片并复制数据来解决此问题。 另一种选择是使用完整切片表达式。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;bytes\u0026#34; ) func main() { path := []byte(\u0026#34;AAAA/BBBBBBBBB\u0026#34;) sepIndex := bytes.IndexByte(path,\u0026#39;/\u0026#39;) dir1 := path[:sepIndex:sepIndex] //完整切片表达式 dir2 := path[sepIndex+1:] fmt.Println(\u0026#34;dir1 =\u0026gt;\u0026#34;,string(dir1)) //prints: dir1 =\u0026gt; AAAA fmt.Println(\u0026#34;dir2 =\u0026gt;\u0026#34;,string(dir2)) //prints: dir2 =\u0026gt; BBBBBBBBB dir1 = append(dir1,\u0026#34;suffix\u0026#34;...) path = bytes.Join([][]byte{dir1,dir2},[]byte{\u0026#39;/\u0026#39;}) fmt.Println(\u0026#34;dir1 =\u0026gt;\u0026#34;,string(dir1)) //prints: dir1 =\u0026gt; AAAAsuffix fmt.Println(\u0026#34;dir2 =\u0026gt;\u0026#34;,string(dir2)) //prints: dir2 =\u0026gt; BBBBBBBBB (ok now) fmt.Println(\u0026#34;new path =\u0026gt;\u0026#34;,string(path)) } 完整切片表达式中的额外参数控制新切片的容量。 现在追加到该切片的数据将触发切片扩容,而不是覆盖第二个片中的数据。\n旧的切片 # 级别:中等 多个切片可以引用相同的数据。 例如当你使用现有切片创建新切片时,可能会发生这种情况。 如果程序依靠此行为来正常运行,那么将需要担心的旧的切片。\n在某些时候,当原始数组无法容纳更多新数据时,将数据添加到切片将导致新的数组扩容。现在其他切片将指向旧数组(包含旧数据)。\nimport \u0026#34;fmt\u0026#34; func main() { s1 := []int{1,2,3} fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3] s2 := s1[1:] fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3] for i := range s2 { s2[i] += 20 } //仍然引用相同的数组 fmt.Println(s1) //prints [1 22 23] fmt.Println(s2) //prints [22 23] s2 = append(s2,4) for i := range s2 { s2[i] += 10 } //s1 is now \u0026#34;stale\u0026#34; fmt.Println(s1) //prints [1 22 23] fmt.Println(s2) //prints [32 33 14] } 类型声明和方法 # 级别:中级 通过从现有 (非接口) 类型定义新类型来创建类型声明时,你不会继承为该现有类型定义的方法。\n失败:\npackage main import \u0026#34;sync\u0026#34; type myMutex sync.Mutex func main() { var mtx myMutex mtx.Lock() //error mtx.Unlock() //error } 编译错误:\n /tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex has no field or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex has no field or method Unlock)\n 如果确实需要原始类型的方法,则可以定义一个将原始类型嵌入为匿名字段的新结构类型。\n作品:\npackage main import \u0026#34;sync\u0026#34; type myLocker struct { sync.Mutex } func main() { var lock myLocker lock.Lock() //ok lock.Unlock() //ok } 接口类型声明也保留其方法集。\n作品:\npackage main import \u0026#34;sync\u0026#34; type myLocker sync.Locker func main() { var lock myLocker = new(sync.Mutex) lock.Lock() //ok lock.Unlock() //ok } 突破 “for switch” 和 “ for select” 代码块 # 级别:中级 没有标签的 “break” 语句只会使你脱离内部 switch /select 块。如果不能使用 “ return” 语句,则为外循环定义标签是第二件事。\npackage main import \u0026#34;fmt\u0026#34; func main() { loop: for { switch { case true: fmt.Println(\u0026#34;breaking out...\u0026#34;) break loop } } fmt.Println(\u0026#34;out!\u0026#34;) } “goto” 语句也可以解决问题。\n句中的迭代变量和闭包 # 级别:中级 这是 Go 中最常见的陷阱。for 语句中的迭代变量在每次迭代中都会重复使用。这意味着在 for 循环中创建的每个闭包 (aka 函数文字) 都将引用相同的变量 (它们将在这些 goroutine 开始执行时获得该变量的值)。\n不正确:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { data := []string{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;,\u0026#34;three\u0026#34;} for _,v := range data { go func() { fmt.Println(v) }() } time.Sleep(3 * time.Second) //goroutines print: three, three, three } 最简单的解决方案 (不需要对 goroutine 进行任何更改) 是将当前迭代变量值保存在 for 循环块内的局部变量中。\n作品:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { data := []string{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;,\u0026#34;three\u0026#34;} for _,v := range data { vcopy := v // go func() { fmt.Println(vcopy) }() } time.Sleep(3 * time.Second) //goroutines print: one, two, three } 另一种解决方案是将当前迭代变量作为参数传递给匿名 goroutine。\n作品:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { data := []string{\u0026#34;one\u0026#34;,\u0026#34;two\u0026#34;,\u0026#34;three\u0026#34;} for _,v := range data { go func(in string) { fmt.Println(in) }(v) } time.Sleep(3 * time.Second) //goroutines print: one, two, three } 这是陷阱的稍微复杂一点的版本。\n不正确:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type field struct { name string } func (p *field) print() { fmt.Println(p.name) } func main() { data := []field{{\u0026#34;one\u0026#34;},{\u0026#34;two\u0026#34;},{\u0026#34;three\u0026#34;}} for _,v := range data { go v.print() } time.Sleep(3 * time.Second) //goroutines print: three, three, three } 作品:\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type field struct { name string } func (p *field) print() { fmt.Println(p.name) } func main() { data := []field{{\u0026#34;one\u0026#34;},{\u0026#34;two\u0026#34;},{\u0026#34;three\u0026#34;}} for _,v := range data { v := v go v.print() } time.Sleep(3 * time.Second) //goroutines print: one, two, three } 你认为运行此代码时会看到什么 (为什么)?\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type field struct { name string } func (p *field) print() { fmt.Println(p.name) } func main() { data := []*field{{\u0026#34;one\u0026#34;},{\u0026#34;two\u0026#34;},{\u0026#34;three\u0026#34;}} for _,v := range data { go v.print() } time.Sleep(3 * time.Second) } 延迟函数调用参数评估 # 级别:中级 在评估 defer 语句时 (而不是在函数实际执行时),评估延迟函数调用的参数。延迟方法调用时,将应用相同的规则。结构值也与显式方法参数和封闭变量一起保存。\npackage main import \u0026#34;fmt\u0026#34; func main() { var i int = 1 defer fmt.Println(\u0026#34;result =\u0026gt;\u0026#34;,func() int { return i * 2 }()) i++ //prints: result =\u0026gt; 2 (not ok if you expected 4) } 如果具有指针参数,则可以更改它们指向的值,因为在评估 defer 语句时仅保存指针。\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { i := 1 defer func (in *int) { fmt.Println(\u0026#34;result =\u0026gt;\u0026#34;, *in) }(\u0026amp;i) i = 2 //prints: result =\u0026gt; 2 } 延迟函数调用执行 # 级别:中级 延迟的调用在包含函数的末尾 (以相反的顺序) 而不是在包含代码块的末尾执行。对于新的 Go 开发人员来说,这是一个容易犯的错误,将延迟的代码执行规则与变量作用域规则混为一谈。如果你具有一个长期运行的函数,且该函数具有 for 循环,该循环试图在每次迭代中延迟 defer 资源清理调用,则可能会成为问题。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; ) func main() { if len(os.Args) != 2 { os.Exit(-1) } start, err := os.Stat(os.Args[1]) if err != nil || !start.IsDir(){ os.Exit(-1) } var targets []string filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error { if err != nil { return err } if !fi.Mode().IsRegular() { return nil } targets = append(targets,fpath) return nil }) for _,target := range targets { f, err := os.Open(target) if err != nil { fmt.Println(\u0026#34;bad target:\u0026#34;,target,\u0026#34;error:\u0026#34;,err) //prints error: too many open files break } defer f.Close() //will not be closed at the end of this code block //do something with the file... } } 解决该问题的一种方法是将代码块包装在一个函数中。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; ) func main() { if len(os.Args) != 2 { os.Exit(-1) } start, err := os.Stat(os.Args[1]) if err != nil || !start.IsDir(){ os.Exit(-1) } var targets []string filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error { if err != nil { return err } if !fi.Mode().IsRegular() { return nil } targets = append(targets,fpath) return nil }) for _,target := range targets { func() { f, err := os.Open(target) if err != nil { fmt.Println(\u0026#34;bad target:\u0026#34;,target,\u0026#34;error:\u0026#34;,err) return } defer f.Close() //ok //do something with the file... }() } } 另一种方法是删除 defer 语句\n失败类型断言 # 级别:中级 失败的类型断言将为断言语句中使用的目标类型返回「零值」。当它与影子变量混合在一起时,可能导致意外行为。\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { var data interface{} = \u0026#34;great\u0026#34; if data, ok := data.(int); ok { fmt.Println(\u0026#34;[is an int] value =\u0026gt;\u0026#34;,data) } else { fmt.Println(\u0026#34;[not an int] value =\u0026gt;\u0026#34;,data) //prints: [not an int] value =\u0026gt; 0 (not \u0026#34;great\u0026#34;) } } 正确的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { var data interface{} = \u0026#34;great\u0026#34; if res, ok := data.(int); ok { fmt.Println(\u0026#34;[is an int] value =\u0026gt;\u0026#34;,res) } else { fmt.Println(\u0026#34;[not an int] value =\u0026gt;\u0026#34;,data) //prints: [not an int] value =\u0026gt; great (as expected) } } 阻塞的 Goroutines 和资源泄漏 # 级别:中级 Rob Pike 在 Google I/O 大会上的演讲 「Go Concurrency Patterns」 谈到了许多基本的并发模式。从多个目标中获取第一个结果就是其中之一。\nfunc First(query string, replicas ...Search) Result { c := make(chan Result) searchReplica := func(i int) { c \u0026lt;- replicas[i](query) } for i := range replicas { go searchReplica(i) } return \u0026lt;-c } 该函数为每个搜索副本启动 goroutines。每个 goroutine 将其搜索结果发送到结果通道。返回结果通道的第一个值。\n其他 goroutines 的结果如何?那 goroutines 本身呢?\nFirst() 函数中的结果通道未缓冲。这意味着仅第一个 goroutine 返回。所有其他 goroutine 都被困在尝试发送结果。这意味着,如果你有多个副本,则每个调用都会泄漏资源。\n为了避免泄漏,你需要确保所有 goroutine 都退出。一种潜在的解决方案是使用足够大的缓冲结果通道来保存所有结果。\nfunc First(query string, replicas ...Search) Result { c := make(chan Result,len(replicas)) searchReplica := func(i int) { c \u0026lt;- replicas[i](query) } for i := range replicas { go searchReplica(i) } return \u0026lt;-c } 另一种可能的解决方案是使用 select 语句和 default 大小写以及可保存一个值的缓冲结果通道。default 情况确保即使结果通道无法接收消息,goroutine 也不会卡住。\nfunc First(query string, replicas ...Search) Result { c := make(chan Result,1) searchReplica := func(i int) { select { case c \u0026lt;- replicas[i](query): default: } } for i := range replicas { go searchReplica(i) } return \u0026lt;-c } 你还可以使用特殊的取消通道来中断工作。\nfunc First(query string, replicas ...Search) Result { c := make(chan Result) done := make(chan struct{}) defer close(done) searchReplica := func(i int) { select { case c \u0026lt;- replicas[i](query): case \u0026lt;- done: } } for i := range replicas { go searchReplica(i) } return \u0026lt;-c } 为什么演示文稿中包含这些错误? Rob Pike 只是不想使幻灯片复杂化。这是有道理的,但是对于新的 Go 开发人员来说可能是个问题,他们会按原样使用该代码,而不认为它可能会出现问题。\n相同地址的不同零大小变量 # 级别:中级 如果你有两个不同的变量,它们不应该有不同的地址吗?好吧,Go 并不是这样:-) 如果变量大小为零,它们可能会在内存中共享完全相同的地址。\npackage main import ( \u0026#34;fmt\u0026#34; ) type data struct { } func main() { a := \u0026amp;data{} b := \u0026amp;data{} if a == b { fmt.Printf(\u0026#34;same address - a=%p b=%p\\n\u0026#34;,a,b) //prints: same address - a=0x1953e4 b=0x1953e4 } } iota 的第一次使用并不总是从零开始 # 级别:中级 它可能看起来像是一个 iota 标识符就像一个增量运算符。开始一个新的常量声明,第一次使用 iota 时得到 0,第二次使用时得到 1,依此类推。但情况并非总是如此。\npackage main import ( \u0026#34;fmt\u0026#34; ) const ( azero = iota aone = iota ) const ( info = \u0026#34;processing\u0026#34; bzero = iota bone = iota ) func main() { fmt.Println(azero,aone) //prints: 0 1 fmt.Println(bzero,bone) //prints: 1 2 } iota 实际上是常量声明块中当前行的索引运算符,因此,如果首次使用 iota 不是常量声明块中的第一行,则初始值将不为零。\n在值实例上使用指针接收器方法 # 级别:高级 只要该值是可寻址的,就可以在该值上调用指针接收器方法。换句话说,在某些情况下,你不需要该方法的值接收器版本。\n但是,并非每个变量都是可寻址的。map 元素不可寻址。通过接口引用的变量也是不可寻址的。\npackage main import \u0026#34;fmt\u0026#34; type data struct { name string } func (p *data) print() { fmt.Println(\u0026#34;name:\u0026#34;,p.name) } type printer interface { print() } func main() { d1 := data{\u0026#34;one\u0026#34;} d1.print() //ok var in printer = data{\u0026#34;two\u0026#34;} //error in.print() m := map[string]data {\u0026#34;x\u0026#34;:data{\u0026#34;three\u0026#34;}} m[\u0026#34;x\u0026#34;].print() //error } 编译错误:\n /tmp/sandbox017696142/main.go:21: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver)\n/tmp/sandbox017696142/main.go:25: cannot call pointer method on m[\u0026ldquo;x\u0026rdquo;] /tmp/sandbox017696142/main.go:25: cannot take the address of m[\u0026ldquo;x\u0026rdquo;]\n 更新 map 值字段 # 级别:高级 如果你具有结构值 map,则无法更新单个结构字段。\n失败的范例:\npackage main type data struct { name string } func main() { m := map[string]data {\u0026#34;x\u0026#34;:{\u0026#34;one\u0026#34;}} m[\u0026#34;x\u0026#34;].name = \u0026#34;two\u0026#34; //error } 编译错误:\n /tmp/sandbox380452744/main.go:9: cannot assign to m[\u0026ldquo;x\u0026rdquo;].name\n 它不会工作,因为 map 元素不可寻址。\n对于 Go 新手开发者,可能会感到困惑,slice 元素是可寻址的。\npackage main import \u0026#34;fmt\u0026#34; type data struct { name string } func main() { s := []data {{\u0026#34;one\u0026#34;}} s[0].name = \u0026#34;two\u0026#34; //ok fmt.Println(s) //prints: [{two}] } 请注意,前一阵子可以在其中一个 Go 编译器 (gccgo) 中更新 map 元素字段,但是该行为很快得到解决:-) 它也被认为是 Go 1.3 的潜在功能。当时还不足以提供支持,因此它仍在待办事项清单上。\n首先解决的是使用临时变量。\npackage main import \u0026#34;fmt\u0026#34; type data struct { name string } func main() { m := map[string]data {\u0026#34;x\u0026#34;:{\u0026#34;one\u0026#34;}} r := m[\u0026#34;x\u0026#34;] r.name = \u0026#34;two\u0026#34; m[\u0026#34;x\u0026#34;] = r fmt.Printf(\u0026#34;%v\u0026#34;,m) //prints: map[x:{two}] } 另一个解决方法是使用指针映射。\npackage main import \u0026#34;fmt\u0026#34; type data struct { name string } func main() { m := map[string]*data {\u0026#34;x\u0026#34;:{\u0026#34;one\u0026#34;}} m[\u0026#34;x\u0026#34;].name = \u0026#34;two\u0026#34; //ok fmt.Println(m[\u0026#34;x\u0026#34;]) //prints: \u0026amp;{two} } 顺便说一句,运行此代码会发生什么?\npackage main type data struct { name string } func main() { m := map[string]*data {\u0026#34;x\u0026#34;:{\u0026#34;one\u0026#34;}} m[\u0026#34;z\u0026#34;].name = \u0026#34;what?\u0026#34; //??? } 「nil」接口和「nil」接口值 # 级别:高级 这是 Go 语言中第二常见的陷阱,因为即使接口看起来像指针,它们也不是指针。接口变量仅在其类型和值字段为「nil」时才为「nil」。\n接口类型和值字段基于用于创建相应接口变量的变量的类型和值进行填充。当你尝试检查接口变量是否等于「nil」时,这可能导致意外的行为。\npackage main import \u0026#34;fmt\u0026#34; func main() { var data *byte var in interface{} fmt.Println(data,data == nil) //prints: \u0026lt;nil\u0026gt; true fmt.Println(in,in == nil) //prints: \u0026lt;nil\u0026gt; true in = data fmt.Println(in,in == nil) //prints: \u0026lt;nil\u0026gt; false //\u0026#39;data\u0026#39; is \u0026#39;nil\u0026#39;, but \u0026#39;in\u0026#39; is not \u0026#39;nil\u0026#39; } 当你具有返回接口的函数时,请当心此陷阱。\n错误的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { doit := func(arg int) interface{} { var result *struct{} = nil if(arg \u0026gt; 0) { result = \u0026amp;struct{}{} } return result } if res := doit(-1); res != nil { fmt.Println(\u0026#34;good result:\u0026#34;,res) //prints: good result: \u0026lt;nil\u0026gt; //\u0026#39;res\u0026#39; is not \u0026#39;nil\u0026#39;, but its value is \u0026#39;nil\u0026#39; } } 正确的范例:\npackage main import \u0026#34;fmt\u0026#34; func main() { doit := func(arg int) interface{} { var result *struct{} = nil if(arg \u0026gt; 0) { result = \u0026amp;struct{}{} } else { return nil //return an explicit \u0026#39;nil\u0026#39; } return result } if res := doit(-1); res != nil { fmt.Println(\u0026#34;good result:\u0026#34;,res) } else { fmt.Println(\u0026#34;bad result (res is nil)\u0026#34;) //here as expected } } 堆栈和堆变量 # 级别:高级 你并不总是知道你的变量是分配在堆栈还是堆上。在 C++ 中,使用 new 运算符创建变量始终意味着你具有堆变量。在 Go 语言中,即使使用 new() 或 make() 函数,编译器仍会决定将变量分配到何处。编译器根据变量的大小和「转义分析」的结果来选择存储变量的位置。这也意味着可以返回对局部变量的引用,而在其他语言 (如 C 或 C++) 中则不可以。\n如果你需要知道变量的分配位置,请将「-m」gc 标志传递给「go build」或「go run」(例如,go run -gcflags -m app.go)。\nGOMAXPROCS,并发和并行 # 级别:高级 Go 1.4 以下版本仅使用一个执行上下文 / OS 线程。这意味着在任何给定时间只能执行一个 goroutine。从 Go 1.5 开始,将执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑 CPU 内核的数量。该数字可能与系统上逻辑 CPU 内核的总数不匹配,具体取决于进程的 CPU 亲和力设置。你可以通过更改 GOMAXPROCS 环境变量或调用 runtime.GOMAXPROCS() 函数来调整此数字。\n常见的误解是 GOMAXPROCS 代表 Go 将用于运行 goroutine 的 CPU 数量。runtime.GOMAXPROCS() 函数文档使这个问题更加混乱。GOMAXPROCS 变量描述 ( golang.org/pkg/runtime/) 在讨论 OS 线程方面做得更好。\n你可以将 GOMAXPROCS 设置为大于 CPU 的数量。从 1.10 版开始,GOMAXPROCS 不再受限制。GOMAXPROCS 的最大值以前是 256,后来在 1.9 中增加到 1024。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; ) func main() { fmt.Println(runtime.GOMAXPROCS(-1)) //prints: X (1 on play.golang.org) fmt.Println(runtime.NumCPU()) //prints: X (1 on play.golang.org) runtime.GOMAXPROCS(20) fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 20 runtime.GOMAXPROCS(300) fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 256 } 读写操作重新排序 # 级别:高级 Go 可以对某些操作进行重新排序,但可以确保 goroutine 中发生该行为的整体行为不会改变。但是,它不能保证跨多个 goroutine 的执行顺序。\npackage main import ( \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; ) var _ = runtime.GOMAXPROCS(3) var a, b int func u1() { a = 1 b = 2 } func u2() { a = 3 b = 4 } func p() { println(a) println(b) } func main() { go u1() go u2() go p() time.Sleep(1 * time.Second) } 如果你多次运行此代码,则可能会看到以下 a 和 b 变量组合:\n 1\n2\n3\n4\n0\n2\n0\n0\n1\n4\n a 和 b 最有趣的组合是「02」。它显示 b 已在 a 之前更新。\n如果你需要跨多个 goroutine 保留读取和写入操作的顺序,则需要使用通道或「sync」包中的适当的方法。\n抢占式调度 # 级别:高级 可能有一个流氓 goroutine 阻止了其他 goroutine 的运行。如果你的 for 循环不允许调度程序运行,则可能发生这种情况。\npackage main import \u0026#34;fmt\u0026#34; func main() { done := false go func(){ done = true }() for !done { } fmt.Println(\u0026#34;done!\u0026#34;) } for 循环不必为空。只要它包含不触发调度程序执行的代码,这将是一个问题。\n调度程序将在 GC,“go” 语句,阻塞通道操作,阻塞系统调用和锁定操作之后运行。当调用非内联函数时,它也可能运行。\npackage main import \u0026#34;fmt\u0026#34; func main() { done := false go func(){ done = true }() for !done { fmt.Println(\u0026#34;not done!\u0026#34;) //not inlined } fmt.Println(\u0026#34;done!\u0026#34;) } 要查明你在 for 循环中调用的函数是否内联,请将 “-m” gc 标志传递给 “ go build” 或 “ go run”(例如,go build -gcflags -m)。\n另一种选择是显式调用调度程序。你可以使用 “运行时” 包中的 Gosched() 函数来完成此操作。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; ) func main() { done := false go func(){ done = true }() for !done { runtime.Gosched() } fmt.Println(\u0026#34;done!\u0026#34;) } 请注意,上面的代码包含一个竞争条件。这样做是故意显示出隐藏的陷阱。\n导入 C 和多行导入块 # 级别:Cgo 你需要导入 “C” 包才能使用 Cgo。你可以单行 import 进行此操作,也可以使用 import 块进行此操作。\npackage main /* #include \u0026lt;stdlib.h\u0026gt; */ import ( \u0026#34;C\u0026#34; ) import ( \u0026#34;unsafe\u0026#34; ) func main() { cs := C.CString(\u0026#34;my go string\u0026#34;) C.free(unsafe.Pointer(cs)) } 如果以 import 块的方式引入此包 ,则无法在同一个块中引入其他包。\npackage main /* #include \u0026lt;stdlib.h\u0026gt; */ import ( \u0026#34;C\u0026#34; \u0026#34;unsafe\u0026#34; ) func main() { cs := C.CString(\u0026#34;my go string\u0026#34;) C.free(unsafe.Pointer(cs)) } 编译错误:\n ./main.go:13:2: could not determine kind of name for C.free\n 在 C 和 Cgo 注释之间不要有空白行 # 级别: Cgo Cgo 的第一个陷阱是:cgo 注释需位于 import C 声明的上方。\npackage main /* #include \u0026lt;stdlib.h\u0026gt; */ import \u0026#34;C\u0026#34; import ( \u0026#34;unsafe\u0026#34; ) func main() { cs := C.CString(\u0026#34;my go string\u0026#34;) C.free(unsafe.Pointer(cs)) } 编译错误:\n ./main.go:15:2: could not determine kind of name for C.free\n 确保在 import C 声明前没有任何空白行。\n不能调用带有可变参数的 C 函数 # level: Cgo 你不能直接调用带有可变参数的 C 函数\npackage main /* #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; */ import \u0026#34;C\u0026#34; import ( \u0026#34;unsafe\u0026#34; ) func main() { cstr := C.CString(\u0026#34;go\u0026#34;) C.printf(\u0026#34;%s\\n\u0026#34;,cstr) //not ok C.free(unsafe.Pointer(cstr)) } 编译错误:\n ./main.go:15:2: unexpected type: \u0026hellip;\n 你需要用已知数量参数的函数封装 C 可变数量参数的函数\npackage main /* #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; void out(char* in) { printf(\u0026#34;%s\\n\u0026#34;, in); } */ import \u0026#34;C\u0026#34; import ( \u0026#34;unsafe\u0026#34; ) func main() { cstr := C.CString(\u0026#34;go\u0026#34;) C.out(cstr) //ok C.free(unsafe.Pointer(cstr)) } "},{"id":31,"href":"/qrcode/","title":"Qrcode","section":"","content":" "},{"id":32,"href":"/todo/","title":"Todo","section":"","content":"文章正在创作中 # 文章即将出炉,请耐心等待\u0026hellip;\u0026hellip;\n你可以:\n 加入我一起完成这篇文章,重新梳理知识体系 欢迎给我提 pr 如果你从来没有提过 pr,可以参考 我给开源项目提pr的过程,这是很重要的一种代码协作能力 再次感谢你的支持\n "},{"id":33,"href":"/tools/readme/","title":"Readme","section":"Tools","content":"golang小工具 # 作用 位置 备注 代码测速 speed.go 可超时缓存 cache.go 任意类型变量缓存,可设置超时时间 线程安全的缓存 map.go 任意类型变量缓存,线程安全 配置proxy static_proxy.go 简单配置http proxy 捕获panic defer_panic.go 重试函数 func.go 可传入函数和重试次数,自动重试,要求返回是否成功 参数 go命令行读取参数 - 文件写入读取 - - 配置文件 - - "},{"id":34,"href":"/%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5/%E6%9E%84%E5%BB%BA%E7%BA%A6%E6%9D%9F/","title":"构建约束","section":"工程化实践s","content":"nosplit 禁止内联 noescape 禁止逃逸 gengrate 生成命令 指定构建的版本,通过文件名或者注释约定代码在指定平台运行时起作用 embed\n"},{"id":35,"href":"/%E7%95%AA%E5%A4%96.%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/flag%E5%8C%85%E8%AF%BB%E5%8F%96%E5%91%BD%E4%BB%A4%E8%A1%8C%E9%85%8D%E7%BD%AE/","title":"Flag包读取命令行配置","section":"番外.常用操作s","content":"简介 # kingpin 功能比 flag 库强大,用法差不多。 相比 flag 库,最重要的一点就是支持不加 - 的调用。 比如一个命令行程序有三个函数分别为 A , B , C ,要实现根据命令行的输入运行不同的函数,如果用flag实现的话应该是下面这种使用方法:\n./cli --method A ./cli --method B ./cli --method C 每次都需要输入 --method ,然而用 kingpin 库实现的话就可以达到下面这种效果:\n./cli A ./cli B ./cli C 节省了很多输入操作。\n使用方法 # go get gopkg.in/alecthomas/kingpin.v2 go mod vendor 这样子 go.mod 文件里就引入了, vendor 文件夹就缓存了此包,然后直接在代码中使用。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;gopkg.in/alecthomas/kingpin.v2\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { var ( listenAddress = kingpin.Flag( \u0026#34;web.listen-address\u0026#34;, \u0026#34;Address on which to expose metrics and web interface.\u0026#34;, ).Default(\u0026#34;:18001\u0026#34;).String() metricsPath = kingpin.Flag( \u0026#34;web.telemetry-path\u0026#34;, \u0026#34;Path under which to expose metrics.\u0026#34;, ).Default(\u0026#34;/metrics\u0026#34;).String() ) kingpin.HelpFlag.Short(\u0026#39;h\u0026#39;) kingpin.Parse() conf.ApiMtncUrl = *apiMtncPath http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`\u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;title\u0026gt;Node Exporter\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;xxx Exporter\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;\u0026lt;a href=\u0026#34; ` + *metricsPath + ` \u0026#34;\u0026gt;Metrics\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt;`)) }) http.Handle(\u0026#34;/metrics\u0026#34;, XXXX.Handler()) if err := http.ListenAndServe(*listenAddress, nil); err != nil { fmt.Printf(\u0026#34;Error occur when start server %v\u0026#34;, err) } } 官方文档参考 package kingpin \n引用 # Golang命令行参数解析库kingpin\n"},{"id":36,"href":"/%E7%95%AA%E5%A4%96.%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/golang%E6%89%93%E9%95%9C%E5%83%8Fdockerfile%E7%9A%84%E5%86%99%E6%B3%95/","title":"Golang打镜像 Dockerfile的写法","section":"番外.常用操作s","content":"引言 # 对于dockerfile而言,何为完美? 我认为应该满足以下三点:\n 体积小 构建快 够安全 PS: 注意!从 Docker 17.05 版本起, Docker 才开始支持容器镜像的多阶段构建(multi-stage build),所以本文所使用 docker 版本必须高于 17.05 (多阶段构建的意思就是把编译的过程也放同一个 Dockerfile 里,不用在自己的开发机或服务器上编译,再把编译出的二进制程序打入镜像)\n可联网的环境 # 根据官方的说法,从 Go 1.13 开始,模块管理模式将是 Go 语言开发的默认模式。\n 我们使用go mod 做包管理,就不需要有任何额外配置\nFROMgolang:1.13.5-alpine3.10 AS builderWORKDIR/buildRUN adduser -u 10001 -D app-runnerENV GOPROXY https://goproxy.cnCOPY go.mod .COPY go.sum .RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -a -o your-application .FROMalpine:3.10 AS finalWORKDIR/appCOPY --from=builder /build/your-application /app/#COPY --from=builder /build/config /app/configCOPY --from=builder /etc/passwd /etc/passwdCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/USERapp-runnerENTRYPOINT [\u0026#34;/app/your-application\u0026#34;]逐行拆解 这里的拆解完全引用 手把手教你写一个完美的Golang Dockerfile\n首先,这个dockerfile分为builder和final两部分。\nbuilder 选择了 golang:1.13.5-alpine3.10 作为编译的基础镜像,相比于 golang:1.13 , 一方面是因为它体积更小,另一方面是我发现 golang:1.13 的编译结果,在 alpine:3.10 中会报 not found 的错误,虽说有人提供了其它的解决方案,但是能直接避免,为啥不避免呢。\nRUN adduser -u 10001 -D app-runner接着是创建了一个 app-runner 的用户, -D 表示无密码。\n此用户的信息是是需要拷到 final 中,作为应用程序的启动用户。这是为了避免使用 container 中的默认用户 root ,那可是有安全漏洞的,详细解释,可以参考这篇 medium 上的文章 Processes In Containers Should Not Run As Root\n再下面的四行,\nENV GOPROXY https://goproxy.cnCOPY go.mod .COPY go.sum .RUN go mod download是配置了国内的代理,安装依赖包了。这里用 go mod download 的好处是下次构建镜像文件时,当go.mod和go.sum没有改变时,它是有缓存的,可以避免重复下载依赖包,加快构建。\nbuilder的最后,就是把当前目录的文件拷过去,编译代码了。\nCOPY . .RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -a -o your-application .final 选择了 alpine:3.10 ,一方面是体积小,只有 5m ;另一方面也是和构建镜像的 alpine 版本保持一致。\n接下来几行没啥说的,就是把构建结果、配置文件(有的话)和用户的相关文件拷过去。\n下面的这步一定不要忘记了,\nUSERapp-runner没有它, container 启动时就是用 root 用户启动了!!! 如果被攻击了,那黑客可是就有 root 权限了(不要问我为啥会被攻击)。\n最后,设置一个 ENTRYPOINT ,完事!\n如果你程序的启动过程比较复杂,或者是要在启动时根据环境变量的值做不同的操作,那还是写个 shell 文件吧。\n离线打包 # # Building stageFROMgolang:1.13.5-alpine3.10 AS builderWORKDIR/build/src/your-applicationRUN adduser -u 10001 -D app-runnerENV GO111MODULE offENV GOPATH /buildCOPY . .RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o your-application main.go#RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o your-application main.go# Production stageFROMalpine:3.10 AS finalWORKDIR/appCOPY --from=builder /build/src/your-application/example/linux /appCOPY --from=builder /build/src/your-application/your-application /app#COPY --from=builder /build/src/your-application/conf /app/confRUN adduser -u 10001 -D app-runnerRUN chmod -R 755 /appENTRYPOINT [\u0026#34;/app/your-application\u0026#34;]如果你的环境是内网,不能连接外网(不能联网),要从外部导入一个 go mod 项目,并运行的时候,肯定会 timeout 在下载项目依赖的包的阶段,实际上依赖包已经放到目录文件,不用下载也能正常运行。为了解决这一问题,我们只需要设置参数 GO111MODULE=off ,然后设置正确的 GOPATH 即可\nENV GO111MODULE offENV GOPATH /build在代码库中需要提前把代码包的中 vendor 更新,在本地执行以下命令,并提交到代码库\ngo mod init your-application go mod vendor 这样就会有离线的 vendor 代码库\n |——vendor └──github.com └──golang.org └──gopkg.in └──modules.txt GO111MODULE=off 无 mod 支持, go 会从 GOPATH 和 vendor 文件夹寻找包。 GO111MODULE=on 模块支持,go 会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod 下载依赖。 GO111MODULE=auto 在 $GOPATH/src 外面且根目录有 go.mod 文件时,开启模块支持。 有可能会遇到的问题 # docker镜像源速度慢 # 如果docker镜像拉取速度太慢,或者拉取不到,可以试试改为国内镜像源地址,参考 这里\n更新docker的yum源 # 如果你发现自己的docker版本低,但是自己的源里面又没有想要的版本,那就需要更新官方的源 参考 这里\n引用 # 手把手教你写一个完美的Golang Dockerfile Golang1.5到Golang1.12包管理:golang vendor 到 go mod 官方golang包管理神器,值得一试!go mod | 编程三分钟\n"},{"id":37,"href":"/%E7%95%AA%E5%A4%96.%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/go%E4%BB%A3%E7%A0%81%E5%9F%BA%E6%9C%AC%E6%A0%87%E5%87%86%E8%A7%84%E8%8C%83/","title":"Go代码基本标准规范","section":"番外.常用操作s","content":"文档 # 不刻意制定详细文档 编码级别文档化,支持一键导出文档 统一的标准和习惯,提高可读性 # 三个统一\n 统一的日志 统一的接口规范:错误码、返回格式、国际化 统一编码习惯 统一编码习惯 # 变量常量\n 变量必须见名知义不得用拼音,且长度在 3-20 个字母 名词必须单数 常量必须全大写 所有程序必须有注释\n提交记录必须指明 tapd 单号,功能,更新记录\n代码文件名\n 必须见名知义,保证内部代码单一职责 文件夹必须与包名保持一致,全小写,尽量使用短命名,不能使用下划线、中划线等字符 函数\n 单个函数长度不超过 50 行 参数个数不要超过 5 个(参数过多通常意味着缺少封装,不易维护,容易出错) 函数返回值个数不要超过 3 个,如果超过,建议将其中关系密切的返回值参数封装成一个结构体。 格式化要求 提交代码时,必须使用 gofmt 对代码进行格式化 提交代码时,必须使用 golint 对代码进行检查。 字符串形式的 json 时,使用反单引号,而不是双引号。\n\u0026#34;{\\\u0026#34;key\\\u0026#34;:\\\u0026#34;value\\\u0026#34;}\u0026#34; 改为格式更清晰的:\n` { \u0026#34;key\u0026#34;:\u0026#34;value\u0026#34; } ` 接入自动格式化检查 # 接入自动化代码缺陷扫描 # "},{"id":38,"href":"/%E7%95%AA%E5%A4%96.%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/go%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E5%A4%A7%E5%85%A8/","title":"Go文件操作大全","section":"番外.常用操作s","content":"Go 官方库的文件操作分散在多个包中,比如os、ioutil包,我本来想写一篇总结性的 Go 文件操作的文章,却发现已经有人 2015 年已经写了一篇这样的文章,写的非常好,所以我翻译成了中文,强烈推荐你阅读一下。\n原文: Working with Files in Go, 作者: NanoDano\n介绍 # 万物皆文件 # UNIX 的一个基础设计就是\u0026quot;万物皆文件\u0026quot;(everything is a file)。我们不必知道一个文件到底映射成什么,操作系统的设备驱动抽象成文件。操作系统为设备提供了文件格式的接口。\nGo 语言中的 reader 和 writer 接口也类似。我们只需简单的读写字节,不必知道 reader 的数据来自哪里,也不必知道 writer 将数据发送到哪里。\n你可以在/dev下查看可用的设备,有些可能需要较高的权限才能访问。\n基本操作 # 创建空文件 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) var ( newFile *os.File err error ) func main() { newFile, err = os.Create(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } log.Println(newFile) newFile.Close() } Truncate 文件 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 裁剪一个文件到100个字节。 // 如果文件本来就少于100个字节,则文件中原始内容得以保留,剩余的字节以null字节填充。 // 如果文件本来超过100个字节,则超过的字节会被抛弃。 // 这样我们总是得到精确的100个字节的文件。 // 传入0则会清空文件。 err := os.Truncate(\u0026#34;test.txt\u0026#34;, 100) if err != nil { log.Fatal(err) } } 得到文件信息 # package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) var ( fileInfo os.FileInfo err error ) func main() { // 如果文件不存在,则返回错误 fileInfo, err = os.Stat(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;File name:\u0026#34;, fileInfo.Name()) fmt.Println(\u0026#34;Size in bytes:\u0026#34;, fileInfo.Size()) fmt.Println(\u0026#34;Permissions:\u0026#34;, fileInfo.Mode()) fmt.Println(\u0026#34;Last modified:\u0026#34;, fileInfo.ModTime()) fmt.Println(\u0026#34;Is Directory: \u0026#34;, fileInfo.IsDir()) fmt.Printf(\u0026#34;System interface type: %T\\\\n\u0026#34;, fileInfo.Sys()) fmt.Printf(\u0026#34;System info: %+v\\\\n\\\\n\u0026#34;, fileInfo.Sys()) } 重命名和移动 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { originalPath := \u0026#34;test.txt\u0026#34; newPath := \u0026#34;test2.txt\u0026#34; err := os.Rename(originalPath, newPath) if err != nil { log.Fatal(err) } } 译者按: rename 和 move 原理一样\n 删除文件 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { err := os.Remove(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } } 打开和关闭文件 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 简单地以只读的方式打开。下面的例子会介绍读写的例子。 file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } file.Close() // OpenFile提供更多的选项。 // 最后一个参数是权限模式permission mode // 第二个是打开时的属性 file, err = os.OpenFile(\u0026#34;test.txt\u0026#34;, os.O_APPEND, 0666) if err != nil { log.Fatal(err) } file.Close() // 下面的属性可以单独使用,也可以组合使用。 // 组合使用时可以使用 OR 操作设置 OpenFile的第二个参数,例如: // os.O\\_CREATE|os.O\\_APPEND // 或者 os.O\\_CREATE|os.O\\_TRUNC|os.O_WRONLY // os.O_RDONLY // 只读 // os.O_WRONLY // 只写 // os.O_RDWR // 读写 // os.O_APPEND // 往文件中添建(Append) // os.O_CREATE // 如果文件不存在则先创建 // os.O_TRUNC // 文件打开时裁剪文件 // os.O\\_EXCL // 和O\\_CREATE一起使用,文件不能存在 // os.O_SYNC // 以同步I/O的方式打开 } 译者按:熟悉 Linux 的读者应该很熟悉权限模式,通过 Linux 命令chmod可以更改文件的权限\nhttps://www.linux.com/learn/understanding-linux-file-permissions\n补充了原文未介绍的 flag\n 检查文件是否存在 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) var ( fileInfo *os.FileInfo err error ) func main() { // 文件不存在则返回error fileInfo, err := os.Stat(\u0026#34;test.txt\u0026#34;) if err != nil { if os.IsNotExist(err) { log.Fatal(\u0026#34;File does not exist.\u0026#34;) } } log.Println(\u0026#34;File does exist. File information:\u0026#34;) log.Println(fileInfo) } 检查读写权限 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 这个例子测试写权限,如果没有写权限则返回error。 // 注意文件不存在也会返回error,需要检查error的信息来获取到底是哪个错误导致。 file, err := os.OpenFile(\u0026#34;test.txt\u0026#34;, os.O_WRONLY, 0666) if err != nil { if os.IsPermission(err) { log.Println(\u0026#34;Error: Write permission denied.\u0026#34;) } } file.Close() // 测试读权限 file, err = os.OpenFile(\u0026#34;test.txt\u0026#34;, os.O_RDONLY, 0666) if err != nil { if os.IsPermission(err) { log.Println(\u0026#34;Error: Read permission denied.\u0026#34;) } } file.Close() } 改变权限、拥有者、时间戳 # package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;time\u0026#34; ) func main() { // 使用Linux风格改变文件权限 err := os.Chmod(\u0026#34;test.txt\u0026#34;, 0777) if err != nil { log.Println(err) } // 改变文件所有者 err = os.Chown(\u0026#34;test.txt\u0026#34;, os.Getuid(), os.Getgid()) if err != nil { log.Println(err) } // 改变时间戳 twoDaysFromNow := time.Now().Add(48 \\* time.Hour) lastAccessTime := twoDaysFromNow lastModifyTime := twoDaysFromNow err = os.Chtimes(\u0026#34;test.txt\u0026#34;, lastAccessTime, lastModifyTime) if err != nil { log.Println(err) } } 硬链接和软链接 # 一个普通的文件是一个指向硬盘的 inode 的地方。\n硬链接创建一个新的指针指向同一个地方。只有所有的链接被删除后文件才会被删除。硬链接只在相同的文件系统中才工作。你可以认为一个硬链接是一个正常的链接。\nsymbolic link,又叫软连接,和硬链接有点不一样,它不直接指向硬盘中的相同的地方,而是通过名字引用其它文件。他们可以指向不同的文件系统中的不同文件。并不是所有的操作系统都支持软链接。\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 创建一个硬链接。 // 创建后同一个文件内容会有两个文件名,改变一个文件的内容会影响另一个。 // 删除和重命名不会影响另一个。 err := os.Link(\u0026#34;original.txt\u0026#34;, \u0026#34;original_also.txt\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;creating sym\u0026#34;) // Create a symlink err = os.Symlink(\u0026#34;original.txt\u0026#34;, \u0026#34;original_sym.txt\u0026#34;) if err != nil { log.Fatal(err) } // Lstat返回一个文件的信息,但是当文件是一个软链接时,它返回软链接的信息,而不是引用的文件的信息。 // Symlink在Windows中不工作。 fileInfo, err := os.Lstat(\u0026#34;original_sym.txt\u0026#34;) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Link info: %+v\u0026#34;, fileInfo) //改变软链接的拥有者不会影响原始文件。 err = os.Lchown(\u0026#34;original_sym.txt\u0026#34;, os.Getuid(), os.Getgid()) if err != nil { log.Fatal(err) } } 读写 # 复制文件 # package main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;io\u0026#34; ) func main() { // 打开原始文件 originalFile, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } defer originalFile.Close() // 创建新的文件作为目标文件 newFile, err := os.Create(\u0026#34;test_copy.txt\u0026#34;) if err != nil { log.Fatal(err) } defer newFile.Close() // 从源中复制字节到目标文件 bytesWritten, err := io.Copy(newFile, originalFile) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Copied %d bytes.\u0026#34;, bytesWritten) // 将文件内容flush到硬盘中 err = newFile.Sync() if err != nil { log.Fatal(err) } } 跳转到文件指定位置(Seek) # package main import ( \u0026#34;os\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; ) func main() { file, _ := os.Open(\u0026#34;test.txt\u0026#34;) defer file.Close() // 偏离位置,可以是正数也可以是负数 var offset int64 = 5 // 用来计算offset的初始位置 // 0 = 文件开始位置 // 1 = 当前位置 // 2 = 文件结尾处 var whence int = 0 newPosition, err := file.Seek(offset, whence) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Just moved to 5:\u0026#34;, newPosition) // 从当前位置回退两个字节 newPosition, err = file.Seek(-2, 1) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Just moved back two:\u0026#34;, newPosition) // 使用下面的技巧得到当前的位置 currentPosition, err := file.Seek(0, 1) fmt.Println(\u0026#34;Current position:\u0026#34;, currentPosition) // 转到文件开始处 newPosition, err = file.Seek(0, 0) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Position after seeking 0,0:\u0026#34;, newPosition) } 写文件 # 可以使用os包写入一个打开的文件。\n因为 Go 可执行包是静态链接的可执行文件,你 import 的每一个包都会增加你的可执行文件的大小。其它的包如io、` ioutil `、` bufio `提供了一些方法,但是它们不是必须的。\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; ) func main() { // 可写方式打开文件 file, err := os.OpenFile( \u0026#34;test.txt\u0026#34;, os.O\\_WRONLY|os.O\\_TRUNC|os.O_CREATE, 0666, ) if err != nil { log.Fatal(err) } defer file.Close() // 写字节到文件中 byteSlice := \\[\\]byte(\u0026#34;Bytes!\\\\n\u0026#34;) bytesWritten, err := file.Write(byteSlice) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Wrote %d bytes.\\\\n\u0026#34;, bytesWritten) } 快写文件 # ioutil包有一个非常有用的方法WriteFile()可以处理创建/打开文件、写字节 slice 和关闭文件一系列的操作。如果你需要简洁快速地写字节 slice 到文件中,你可以使用它。\npackage main import ( \u0026#34;io/ioutil\u0026#34; \u0026#34;log\u0026#34; ) func main() { err := ioutil.WriteFile(\u0026#34;test.txt\u0026#34;, \\[\\]byte(\u0026#34;Hi\\\\n\u0026#34;), 0666) if err != nil { log.Fatal(err) } } 使用缓存写 # bufio包提供了带缓存功能的 writer,所以你可以在写字节到硬盘前使用内存缓存。当你处理很多的数据很有用,因为它可以节省操作硬盘 I/O 的时间。在其它一些情况下它也很有用,比如你每次写一个字节,把它们攒在内存缓存中,然后一次写入到硬盘中,减少硬盘的磨损以及提升性能。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;bufio\u0026#34; ) func main() { // 打开文件,只写 file, err := os.OpenFile(\u0026#34;test.txt\u0026#34;, os.O_WRONLY, 0666) if err != nil { log.Fatal(err) } defer file.Close() // 为这个文件创建buffered writer bufferedWriter := bufio.NewWriter(file) // 写字节到buffer bytesWritten, err := bufferedWriter.Write( \\[\\]byte{65, 66, 67}, ) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Bytes written: %d\\\\n\u0026#34;, bytesWritten) // 写字符串到buffer // 也可以使用 WriteRune() 和 WriteByte() bytesWritten, err = bufferedWriter.WriteString( \u0026#34;Buffered string\\\\n\u0026#34;, ) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Bytes written: %d\\\\n\u0026#34;, bytesWritten) // 检查缓存中的字节数 unflushedBufferSize := bufferedWriter.Buffered() log.Printf(\u0026#34;Bytes buffered: %d\\\\n\u0026#34;, unflushedBufferSize) // 还有多少字节可用(未使用的缓存大小) bytesAvailable := bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Available buffer: %d\\\\n\u0026#34;, bytesAvailable) // 写内存buffer到硬盘 bufferedWriter.Flush() // 丢弃还没有flush的缓存的内容,清除错误并把它的输出传给参数中的writer // 当你想将缓存传给另外一个writer时有用 bufferedWriter.Reset(bufferedWriter) bytesAvailable = bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Available buffer: %d\\\\n\u0026#34;, bytesAvailable) // 重新设置缓存的大小。 // 第一个参数是缓存应该输出到哪里,这个例子中我们使用相同的writer。 // 如果我们设置的新的大小小于第一个参数writer的缓存大小, 比如10,我们不会得到一个10字节大小的缓存, // 而是writer的原始大小的缓存,默认是4096。 // 它的功能主要还是为了扩容。 bufferedWriter = bufio.NewWriterSize( bufferedWriter, 8000, ) // resize后检查缓存的大小 bytesAvailable = bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Available buffer: %d\\\\n\u0026#34;, bytesAvailable) } 读取最多 N 个字节 # os.File提供了文件操作的基本功能, 而io、ioutil、bufio提供了额外的辅助函数。\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; ) func main() { // 打开文件,只读 file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } defer file.Close() // 从文件中读取len(b)字节的文件。 // 返回0字节意味着读取到文件尾了 // 读取到文件会返回io.EOF的error byteSlice := make(\\[\\]byte, 16) bytesRead, err := file.Read(byteSlice) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Number of bytes read: %d\\\\n\u0026#34;, bytesRead) log.Printf(\u0026#34;Data read: %s\\\\n\u0026#34;, byteSlice) } 读取正好 N 个字节 # package main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;io\u0026#34; ) func main() { // Open file for reading file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } // file.Read()可以读取一个小文件到大的byte slice中, // 但是io.ReadFull()在文件的字节数小于byte slice字节数的时候会返回错误 byteSlice := make(\\[\\]byte, 2) numBytesRead, err := io.ReadFull(file, byteSlice) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Number of bytes read: %d\\\\n\u0026#34;, numBytesRead) log.Printf(\u0026#34;Data read: %s\\\\n\u0026#34;, byteSlice) } 读取至少 N 个字节 # package main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;io\u0026#34; ) func main() { // 打开文件,只读 file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } byteSlice := make(\\[\\]byte, 512) minBytes := 8 // io.ReadAtLeast()在不能得到最小的字节的时候会返回错误,但会把已读的文件保留 numBytesRead, err := io.ReadAtLeast(file, byteSlice, minBytes) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Number of bytes read: %d\\\\n\u0026#34;, numBytesRead) log.Printf(\u0026#34;Data read: %s\\\\n\u0026#34;, byteSlice) } 读取全部字节 # package main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } // os.File.Read(), io.ReadFull() 和 // io.ReadAtLeast() 在读取之前都需要一个固定大小的byte slice。 // 但ioutil.ReadAll()会读取reader(这个例子中是file)的每一个字节,然后把字节slice返回。 data, err := ioutil.ReadAll(file) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Data as hex: %x\\\\n\u0026#34;, data) fmt.Printf(\u0026#34;Data as string: %s\\\\n\u0026#34;, data) fmt.Println(\u0026#34;Number of bytes read:\u0026#34;, len(data)) } 快读到内存 # package main import ( \u0026#34;log\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { // 读取文件到byte slice中 data, err := ioutil.ReadFile(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Data read: %s\\\\n\u0026#34;, data) } 使用缓存读 # 有缓存写也有缓存读。\n缓存 reader 会把一些内容缓存在内存中。它会提供比os.File和io.Reader更多的函数,缺省的缓存大小是 4096,最小缓存是 16。\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 打开文件,创建buffered reader file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } bufferedReader := bufio.NewReader(file) // 得到字节,当前指针不变 byteSlice := make(\\[\\]byte, 5) byteSlice, err = bufferedReader.Peek(5) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Peeked at 5 bytes: %s\\\\n\u0026#34;, byteSlice) // 读取,指针同时移动 numBytesRead, err := bufferedReader.Read(byteSlice) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Read %d bytes: %s\\\\n\u0026#34;, numBytesRead, byteSlice) // 读取一个字节, 如果读取不成功会返回Error myByte, err := bufferedReader.ReadByte() if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Read 1 byte: %c\\\\n\u0026#34;, myByte) // 读取到分隔符,包含分隔符,返回byte slice dataBytes, err := bufferedReader.ReadBytes(\u0026#39;\\\\n\u0026#39;) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Read bytes: %s\\\\n\u0026#34;, dataBytes) // 读取到分隔符,包含分隔符,返回字符串 dataString, err := bufferedReader.ReadString(\u0026#39;\\\\n\u0026#39;) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Read string: %s\\\\n\u0026#34;, dataString) //这个例子读取了很多行,所以test.txt应该包含多行文本才不至于出错 } 使用 scanner # Scanner是bufio包下的类型,在处理文件中以分隔符分隔的文本时很有用。\n通常我们使用换行符作为分隔符将文件内容分成多行。在 CSV 文件中,逗号一般作为分隔符。\nos.File文件可以被包装成bufio.Scanner,它就像一个缓存 reader。\n我们会调用Scan()方法去读取下一个分隔符,使用Text()或者Bytes()获取读取的数据。\n分隔符可以不是一个简单的字节或者字符,有一个特殊的方法可以实现分隔符的功能,以及将指针移动多少,返回什么数据。\n如果没有定制的SplitFunc提供,缺省的ScanLines会使用newline字符作为分隔符,其它的分隔函数还包括ScanRunes和ScanWords,皆在bufio包中。\n// To define your own split function, match this fingerprint type SplitFunc func(data \\[\\]byte, atEOF bool) (advance int, token \\[\\]byte, err error) // Returning (0, nil, nil) will tell the scanner // to scan again, but with a bigger buffer because // it wasn\u0026#39;t enough data to reach the delimiter 下面的例子中,为一个文件创建了bufio.Scanner,并按照单词逐个读取:\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;bufio\u0026#34; ) func main() { file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } scanner := bufio.NewScanner(file) // 缺省的分隔函数是bufio.ScanLines,我们这里使用ScanWords。 // 也可以定制一个SplitFunc类型的分隔函数 scanner.Split(bufio.ScanWords) // scan下一个token. success := scanner.Scan() if success == false { // 出现错误或者EOF是返回Error err = scanner.Err() if err == nil { log.Println(\u0026#34;Scan completed and reached EOF\u0026#34;) } else { log.Fatal(err) } } // 得到数据,Bytes() 或者 Text() fmt.Println(\u0026#34;First word found:\u0026#34;, scanner.Text()) // 再次调用scanner.Scan()发现下一个token } 压缩 # 打包(zip) 文件 # // This example uses zip but standard library // also supports tar archives package main import ( \u0026#34;archive/zip\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 创建一个打包文件 outFile, err := os.Create(\u0026#34;test.zip\u0026#34;) if err != nil { log.Fatal(err) } defer outFile.Close() // 创建zip writer zipWriter := zip.NewWriter(outFile) // 往打包文件中写文件。 // 这里我们使用硬编码的内容,你可以遍历一个文件夹,把文件夹下的文件以及它们的内容写入到这个打包文件中。 var filesToArchive = \\[\\]struct { Name, Body string } { {\u0026#34;test.txt\u0026#34;, \u0026#34;String contents of file\u0026#34;}, {\u0026#34;test2.txt\u0026#34;, \u0026#34;\\\\x61\\\\x62\\\\x63\\\\n\u0026#34;}, } // 下面将要打包的内容写入到打包文件中,依次写入。 for _, file := range filesToArchive { fileWriter, err := zipWriter.Create(file.Name) if err != nil { log.Fatal(err) } _, err = fileWriter.Write(\\[\\]byte(file.Body)) if err != nil { log.Fatal(err) } } // 清理 err = zipWriter.Close() if err != nil { log.Fatal(err) } } 抽取(unzip) 文件 # // This example uses zip but standard library // also supports tar archives package main import ( \u0026#34;archive/zip\u0026#34; \u0026#34;log\u0026#34; \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; ) func main() { zipReader, err := zip.OpenReader(\u0026#34;test.zip\u0026#34;) if err != nil { log.Fatal(err) } defer zipReader.Close() // 遍历打包文件中的每一文件/文件夹 for _, file := range zipReader.Reader.File { // 打包文件中的文件就像普通的一个文件对象一样 zippedFile, err := file.Open() if err != nil { log.Fatal(err) } defer zippedFile.Close() // 指定抽取的文件名。 // 你可以指定全路径名或者一个前缀,这样可以把它们放在不同的文件夹中。 // 我们这个例子使用打包文件中相同的文件名。 targetDir := \u0026#34;./\u0026#34; extractedFilePath := filepath.Join( targetDir, file.Name, ) // 抽取项目或者创建文件夹 if file.FileInfo().IsDir() { // 创建文件夹并设置同样的权限 log.Println(\u0026#34;Creating directory:\u0026#34;, extractedFilePath) os.MkdirAll(extractedFilePath, file.Mode()) } else { //抽取正常的文件 log.Println(\u0026#34;Extracting file:\u0026#34;, file.Name) outputFile, err := os.OpenFile( extractedFilePath, os.O\\_WRONLY|os.O\\_CREATE|os.O_TRUNC, file.Mode(), ) if err != nil { log.Fatal(err) } defer outputFile.Close() // 通过io.Copy简洁地复制文件内容 _, err = io.Copy(outputFile, zippedFile) if err != nil { log.Fatal(err) } } } } 压缩文件 # // 这个例子中使用gzip压缩格式,标准库还支持zlib, bz2, flate, lzw package main import ( \u0026#34;os\u0026#34; \u0026#34;compress/gzip\u0026#34; \u0026#34;log\u0026#34; ) func main() { outputFile, err := os.Create(\u0026#34;test.txt.gz\u0026#34;) if err != nil { log.Fatal(err) } gzipWriter := gzip.NewWriter(outputFile) defer gzipWriter.Close() // 当我们写如到gizp writer数据时,它会依次压缩数据并写入到底层的文件中。 // 我们不必关心它是如何压缩的,还是像普通的writer一样操作即可。 _, err = gzipWriter.Write(\\[\\]byte(\u0026#34;Gophers rule!\\\\n\u0026#34;)) if err != nil { log.Fatal(err) } log.Println(\u0026#34;Compressed data written to file.\u0026#34;) } 解压缩文件 # // 这个例子中使用gzip压缩格式,标准库还支持zlib, bz2, flate, lzw package main import ( \u0026#34;compress/gzip\u0026#34; \u0026#34;log\u0026#34; \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 打开一个gzip文件。 // 文件是一个reader,但是我们可以使用各种数据源,比如web服务器返回的gzipped内容, // 它的内容不是一个文件,而是一个内存流 gzipFile, err := os.Open(\u0026#34;test.txt.gz\u0026#34;) if err != nil { log.Fatal(err) } gzipReader, err := gzip.NewReader(gzipFile) if err != nil { log.Fatal(err) } defer gzipReader.Close() // 解压缩到一个writer,它是一个file writer outfileWriter, err := os.Create(\u0026#34;unzipped.txt\u0026#34;) if err != nil { log.Fatal(err) } defer outfileWriter.Close() // 复制内容 _, err = io.Copy(outfileWriter, gzipReader) if err != nil { log.Fatal(err) } } 其它 # 临时文件和目录 # ioutil提供了两个函数: TempDir() 和 TempFile()。\n使用完毕后,调用者负责删除这些临时文件和文件夹。\n有一点好处就是当你传递一个空字符串作为文件夹名的时候,它会在操作系统的临时文件夹中创建这些项目(/tmp on Linux)。\nos.TempDir()返回当前操作系统的临时文件夹。\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;io/ioutil\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 在系统临时文件夹中创建一个临时文件夹 tempDirPath, err := ioutil.TempDir(\u0026#34;\u0026#34;, \u0026#34;myTempDir\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Temp dir created:\u0026#34;, tempDirPath) // 在临时文件夹中创建临时文件 tempFile, err := ioutil.TempFile(tempDirPath, \u0026#34;myTempFile.txt\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Temp file created:\u0026#34;, tempFile.Name()) // ... 做一些操作 ... // 关闭文件 err = tempFile.Close() if err != nil { log.Fatal(err) } // 删除我们创建的资源 err = os.Remove(tempFile.Name()) if err != nil { log.Fatal(err) } err = os.Remove(tempDirPath) if err != nil { log.Fatal(err) } } 通过 HTTP 下载文件 # package main import ( \u0026#34;os\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { newFile, err := os.Create(\u0026#34;devdungeon.html\u0026#34;) if err != nil { log.Fatal(err) } defer newFile.Close() url := \u0026#34;http://www.devdungeon.com/archive\u0026#34; response, err := http.Get(url) defer response.Body.Close() // 将HTTP response Body中的内容写入到文件 // Body满足reader接口,因此我们可以使用ioutil.Copy numBytesWritten, err := io.Copy(newFile, response.Body) if err != nil { log.Fatal(err) } log.Printf(\u0026#34;Downloaded %d byte file.\\\\n\u0026#34;, numBytesWritten) } 哈希和摘要 # package main import ( \u0026#34;crypto/md5\u0026#34; \u0026#34;crypto/sha1\u0026#34; \u0026#34;crypto/sha256\u0026#34; \u0026#34;crypto/sha512\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io/ioutil\u0026#34; ) func main() { // 得到文件内容 data, err := ioutil.ReadFile(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } // 计算Hash fmt.Printf(\u0026#34;Md5: %x\\\\n\\\\n\u0026#34;, md5.Sum(data)) fmt.Printf(\u0026#34;Sha1: %x\\\\n\\\\n\u0026#34;, sha1.Sum(data)) fmt.Printf(\u0026#34;Sha256: %x\\\\n\\\\n\u0026#34;, sha256.Sum256(data)) fmt.Printf(\u0026#34;Sha512: %x\\\\n\\\\n\u0026#34;, sha512.Sum512(data)) } 上面的例子复制整个文件内容到内存中,传递给 hash 函数。\n另一个方式是创建一个 hash writer, 使用Write、WriteString、Copy将数据传给它。\n下面的例子使用 md5 hash,但你可以使用其它的 Writer。\npackage main import ( \u0026#34;crypto/md5\u0026#34; \u0026#34;log\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; ) func main() { file, err := os.Open(\u0026#34;test.txt\u0026#34;) if err != nil { log.Fatal(err) } defer file.Close() //创建一个新的hasher,满足writer接口 hasher := md5.New() _, err = io.Copy(hasher, file) if err != nil { log.Fatal(err) } // 计算hash并打印结果。 // 传递 nil 作为参数,因为我们不通参数传递数据,而是通过writer接口。 sum := hasher.Sum(nil) fmt.Printf(\u0026#34;Md5 checksum: %x\\\\n\u0026#34;, sum) } 参考 # Go Standard Library Documentation\n"},{"id":39,"href":"/%E7%95%AA%E5%A4%96.%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C/%E5%88%87%E7%89%87%E6%8E%92%E5%BA%8Fsort%E5%8C%85%E7%9A%84%E4%BD%BF%E7%94%A8/","title":"切片排序sort包的使用","section":"番外.常用操作s","content":"golang的sort包提供了部分切片排序的函数和用户自定义数据集的函数。\n排序切片 # func Example1() { arry := []int{5,8,3,1,4,2,7,6} fmt.Println(arry) sort.Ints(arry) fmt.Println(arry) // Output: // [5 8 3 1 4 2 7 6] // [1 2 3 4 5 6 7 8] } 排序用户自定义数据集 # type Person struct { Name string Age int } func (p Person) String() string { return fmt.Sprintf(\u0026#34;%s: %d\u0026#34;, p.Name, p.Age) } // ByAge implements sort.Interface for []Person based on // the Age field. type ByAge []Person func (a ByAge) Len() int { return len(a) } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByAge) Less(i, j int) bool { return a[i].Age \u0026lt; a[j].Age } func Example2() { people := []Person{ {\u0026#34;Bob\u0026#34;, 31}, {\u0026#34;John\u0026#34;, 42}, {\u0026#34;Michael\u0026#34;, 17}, {\u0026#34;Jenny\u0026#34;, 26}, } fmt.Println(people) sort.Sort(ByAge(people)) fmt.Println(people) // Output: // [Bob: 31 John: 42 Michael: 17 Jenny: 26] // [Michael: 17 Jenny: 26 Bob: 31 John: 42] } 按选定的Key排序自定义数据集 # type stature float32 type weight float32 type Pepole struct { name string h stature w weight } // By is the type of a \u0026#34;less\u0026#34; function that defines the ordering of its Pepole arguments. type By func(p1, p2 *Pepole) bool // Sort is a method on the function type, By, that sorts the argument slice according to the function. func (by By) Sort(p []Pepole) { ps := \u0026amp;pepoleSorter{ pepole: pepole, by: by, // The Sort method\u0026#39;s receiver is the function (closure) that defines the sort order. } sort.Sort(ps) } // pepoleSorter joins a By function and a slice of Pepole to be sorted. type pepoleSorter struct { pepole []Pepole by func(p1, p2 *Pepole) bool // Closure used in the Less method. } // Len is part of sort.Interface. func (s *pepoleSorter) Len() int { return len(s.pepole) } // Swap is part of sort.Interface. func (s *pepoleSorter) Swap(i, j int) { s.pepole[i], s.pepole[j] = s.pepole[j], s.pepole[i] } // Less is part of sort.Interface. It is implemented by calling the \u0026#34;by\u0026#34; closure in the sorter. func (s *pepoleSorter) Less(i, j int) bool { return s.by(\u0026amp;s.pepole[i], \u0026amp;s.pepole[j]) } var pepole = []Pepole{ {\u0026#34;Rose\u0026#34;, 1.58, 66.6}, {\u0026#34;Daisley\u0026#34;, 1.78, 58.4}, {\u0026#34;Lumiya\u0026#34;, 1.65, 57.9}, {\u0026#34;Sola\u0026#34;, 1.68, 55.77}, } // ExampleSortKeys demonstrates a technique for sorting a struct type using programmable sort criteria. func Example_sortKeys() { // Closures that order the Planet structure. name := func(p1, p2 *Pepole) bool { return p1.name \u0026lt; p2.name } stature := func(p1, p2 *Pepole) bool { return p1.h \u0026lt; p2.h } weight := func(p1, p2 *Pepole) bool { return p1.w \u0026lt; p2.w } decreasingWeight := func(p1, p2 *Pepole) bool { return !weight(p1, p2) } // Sort the pepole by the various criteria. By(name).Sort(pepole) fmt.Println(\u0026#34;By name:\u0026#34;, pepole) By(stature).Sort(pepole) fmt.Println(\u0026#34;By stature:\u0026#34;, pepole) By(weight).Sort(pepole) fmt.Println(\u0026#34;By weight:\u0026#34;, pepole) By(decreasingWeight).Sort(pepole) fmt.Println(\u0026#34;By decreasing weight:\u0026#34;, pepole) // Output: // By name: [{Daisley 1.78 58.4} {Lumiya 1.65 57.9} {Rose 1.58 66.6} {Sola 1.68 55.77}] // By stature: [{Rose 1.58 66.6} {Lumiya 1.65 57.9} {Sola 1.68 55.77} {Daisley 1.78 58.4}] // By weight: [{Sola 1.68 55.77} {Lumiya 1.65 57.9} {Daisley 1.78 58.4} {Rose 1.58 66.6}] // By decreasing weight: [{Rose 1.58 66.6} {Daisley 1.78 58.4} {Lumiya 1.65 57.9} {Sola 1.68 55.77}] } 转自 golang切片排序sort包的使用\n"}]