The reason I was enthusiastic about Go is because, at the same time we were starting on Go, I tried to read the C++ 0x proposed standard, that was the convincer for me.
– Ken Thompson
0x00. 前言
1. 背景介绍
A. 起源
首先Google是Go语言的最大企业背书, 几个CS领域的大牛是早期的核心背书, 算是出生在豪门, 所以尽管2012年Go才正式发布, 但是发展非常迅猛, 在熟悉C/C++/Java/Python任一语言后, 都可以开始上手学习Go/Rust, 详细一些的背景参考书籍, 我就不搬运了.
备注: 不建议0基础学Go, 虽然表面说它是没有黑魔法, 但其实是有不少有争议的PL设计, 裸上手看起容易, 实则对很多特性一知半解. 另建议有一定程序语言基础的同学搭配handbook/原理分析看.
B. 环境
Go安装之后有几个关键的环境变量, 建议搞清楚含义, 提前设置好, 不使用默认值.
GOPATH: 类似一个workspace, 默认选择的是~/go, 建议调整为自己单独的开发目录, 核心配置GOROOT: 类似C/Java中的语言安装位置, 这个你安装的时候选择合适的位置就无需调整了
其他的一些GOXxx可以先不调整, 使用默认值, 后续的例子中, 只有前几个会带上包声明, 后续都只会包含关键片段. 入门阶段先还是Code first, 坚持用起来, 不钻设计和细节问题, 因为这可能也是大牛们个人喜好, 每个语言都有自己的想法.
C. 进阶
如果你已经有很好的PL(程序语言)基础, 或者希望对go的实现机制理解透彻, 或者写出优雅的Go代码, 那么推荐以下资料:
2. 上手配置
开源的语言开源的项目,一般认为初学者接触比较好的参考就是官方文档/官方教程之类的.这里也不例外(推荐Vscode+go官方教程本地). 在使用Go开发之前, 有一些基本的环境和VScode插件需要配置一下, 这里一起说了, 避免后续浪费时间折腾环境
官网下载安装Go, 现在Win/Mac有一键的安装包, 简单许多, 最新版本是V1.14.4 (VSCode自行安装)
配置系统环境变量和环境, 配置好后用
go env确认一致 (重点)1
2
3
4
5// 确保以下参数调整后
set GOPROXY=https://goproxy.cn,direct
// 修改env值用"-w" 带上具体的参数名就可以了.
go env -w GO111MODULE=auto配置VSCode-Go插件和相关优化配置,
ms-vscode.go插件会提醒你一键下载所有Go自带的工具集, 下完后存储在$GOPATH/bin1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18"editor.formatOnType": true,
"editor.formatOnSave": true,
// Ensure use bash* shell
"go.buildOnSave": true,
"go.lintOnSave": true,
"go.vetOnSave": true,
"go.useCodeSnippetsOnFunctionSuggest": true,
// "go.useLanguageServer": false,
"go.autocompleteUnimportedPackages": true,
"go.useCodeSnippetsOnFunctionSuggest": false,
"go.formatTool": "goimports",
"editor.fontSize": 18,
// Keep same with OS env
"go.gopath": "/dev/Go",
"go.goroot": "/software/Golang",
"go.buildFlags": [],
"go.lintFlags": [],
"go.vetFlags": []
**更新: **Go1.14开始, 自带的包管理工具module已经GA, 不过目前这不是重点, 先不用多管, 等用熟悉就明白了, 类似gradle/mvn 的官方版.
3. 基本结构
Go有不少和其他常见语言设计不同的地方, 刚开始你可能会容易陷入各种小的细节/坑中, 但是不要前期别太拘泥细节, 先熟悉上手, 之后会好很多.
Go的特点大家都说烂了, 目前看核心就两个, 定位应该是在C和Java之间, 面向网络/分布式开发为主:
- 比起C/C++/Java来说相对简洁. 上手比较简单
- 自带语言层面的的并发, 可以一键体验并发的性能.
然后就安装一下go的编译器,工具跟库.IDE这种吧. 一般Win/Mac上就用VSCode足矣. linux上用vim即可. 并不需要下很重的IDE
仅有约25个关键字: (第一排5个重点关注)
| defer | go | chan | range | select |
|---|---|---|---|---|
| case | break | default | map | struct |
| func | else | goto | package | switch |
| const | fallthrough | if | interface | type |
| continue | for | import | return | var |
然后如果想深入一点了解Go的实现和原理, 这里有一个开源书作者给的结构图, 之后可以参考:
0x01. 基本概念
Go官方教程的话, 现在可以离线下到本地, go get golang.org/x/tour 下载后,例如win上直接 $GOPATH/bin/tour.exe 启动, 它起了一个本地的WebServer, 后续的笔记我尽量写Go中常用且特殊一些的, 其他的跟着官方tutor走就行了.
1. 输出
运行go 程序语法 go run demo.go
- go中标准库的是从
GOROOT下加载的, 第三方的库一般下本地后用绝对路径方式导入, 例如:import "a/b/c" - go中可以单行导包, 但推荐多行
()导入, 并且支持几个特殊的语法糖- 静态导入:
import . "fmt", 然后你可以直接使用Printf(), 类似Java中的静态导入(static import), 但因为这个粒度很粗, 不建议多用. - 别名导入:
import f "fmt", 然后可以f.Printf()访问 - 匿名导入:
import _ os, 普通情况go里所有变量/包定义必须被使用, 这种方式可以让包仅执行init()方法, 但不使用….(还不确定场景是)
- 静态导入:
1 | package main |
- 当go代码编译为一个可执行的程序的时候, package必须是main, 而如果只是编译成库,就没有package限制.
fmt/os/time/log等直接引用的多是Go的标准库, 然后还有相对引用和绝对地址引用, 都挺简单.
2. 包 & 权限
程序运行入口是包main, 多个包的导入可使用语法糖import (), 简单不少
- 跟python/java类似,go把相同功能的代码放到一个目录.称之为包(package)
- 包可以被其他包引用. (main包比较特殊,只能有一个)
- package定义需要放在代码第一行(非注释). (同java)
- import导入的也是包. 跟java的import类似. 只不过这里有黑魔法导多个包..
- 注意与经典OOP不同的是. go的包中可以有函数 (比如
fmt.Println()). 而类如Java中方法必须属于一个类或类实例的. 不要误以为fmt是一个类,Println是一个静态方法了
1 | package main |
注意:这个random的例子中rand.Intn 每次都会返回相同的数字 (为了得到不同的随机数,需要提供一个随机数种子, 可参考 rand.Seed )
访问权限的控制, 不管是方法还是变量, 都通过方法大小写区别.(类似PL中的private/public), 所以在go中方法/变量名不能随便大小写的, 简单说:
大写字母开头的变量跟函数, 可以被其他包访问.
小写开头, 不能被其他包访问
补充: 常量一般其他语言大写. go中也是根据具体需要开放权限后决定.而不用默认大写
3. 变量 & 常量
总的来说, go中支持这几种常用方式定义变量, 尽量别乱混搭, 最后容易坑自己:
- Go的类型在变量名后, 与传统PL设计相反, 见仁见智, 习惯就好.
- 最常见的
var a = 1这样的自动推导, 但令人稍费解的是它并不支持a int = 1, 要指定类型就得var a int32 = 1(rust也类似这样) - 多变量一起声明, 复用一个var, 可以说是个语法糖, 还不错
- 方法体内直接
:=赋值, 也算是个还可以的语法糖 - 多变量一起初始化我只在接受多返回值的时候用, 并不喜欢多个变量放一行声明, 这种”简洁”和省单行方法体的
{}括号差别不大. - 有一个特殊的匿名变量
_, 也可以理解为单纯的占位符, 不分配内存空间, 通常用来存多返回值函数中不需要的. - 最后有一个
new函数创建的指针变量, 注意这里new不是关键字, 而是一个函数 - 常量用
const单独声明, 这个是类似c系的习惯, 还不错.
1 | // 1. var单行声明 |
- go中定义的变量必须被使用,否则报错
- 常量用
const关键字单独声明, 类C的使用方式 - 如果初始化使用表达式的时候,可以省略类型,变量从初始值获得类型
:=是短声明方式,只能用在函数体内, 常用接受多返回值
4.函数与方法
首先要注意, 在Go中函数和方法不是一个含义, 一般把包级别例如fmt.Println() 称为函数, 而把和一个具体类型关联起来的称为方法. 看例子:
1 | package lib |
- 这里
GetTime()是可以通过lib.GetTime()直接调用的, 就叫函数 (类似Java中的静态函数?) - 而
PName()则属于Person结构体的函数, 类似其他语言中普通的类函数 (二级公民) - Go中没有
void关键字, 无返回值不写就好. - 理解之后, 其实不用去硬记那个是
function, 哪个是method, 只是一个叫法罢了.
下面给个Go函数特性的例子:
1 | func add(m int, n int) int { // 函数返回值 |
- 支持多返回值是Go函数最大的特点之一, 从PL角度看也可以理解为一个语法糖, 或者类似
tuple的结构 - 多返回值配合
:=接收, 是Go中最常见的一种用法, 的确也挺方便, 但是总是歪打正着的来接收错误, 个人觉得有点奇怪.. - Go中也支持一个稍特殊一点的
裸返回, 也就是上面split()的例子. - 可变参数这里, 如果想实现类似
void */Object的效果, Go中使用interface{}代替, 虽然这个写法有点奇怪..
1 | func split(sum int) (x, y int) { |
Go还支持匿名函数, 目前看多用在传递函数, 或者回调的时候, 入门可以先略过.
1 | // 第二个参数为匿名函数 |
5.基本类型
go的基本类型类似C的一个变种版, 比较特别一点的是byte和rune, 前者是因为字节在Go中用的很多, 比如string底层是byte而不是传统说的字符(char)
- 整形的不同定义你可以等用到的时候再思考, 平常用int就可以了.
- 变量在定义时没有明确初始化时会自动赋初始值 (包含
0,false,""等) - 有自动装拆包功能的语言可以不同类型自动转换. Go目前是不行的,
int32和int64不能放一起直接操作 - 字符类型一般就用
byte和rune配合单引号表示, 取别名应该也是为了区分整形. 方便理解 - Go中字符串编码都是
utf-8, 一般不容易出现编码问题
1 | bool // 不可与0/1互相转换, 好习惯 |
0x02. 结构与控制
1. 循环 & 判断 & 选择
Go的流程控制也稍显特别, 当然主要是在语法上的特意简洁, 比如:
- 它不允许循环/判断语句带
(), 每个循环/判断都是”裸写”条件, 不过大括号是必要的 - 允许在条件表达式内定义变量, 个人觉得这个习惯并不好, 我一般不用.
- 循环只有
for一个关键字, 后面跟三种类型表达式, 满足不同需求, 个人觉得这个设计还行for condition {}(类似while循环)for a; b; c {}(传统for循环)for range(迭代遍历)
1 | func main(){ |
同理引入不允许() 但是必须有 {} 的if, 这里表达式内赋值可无视. 其他同普通强类型语言.
1 | // 普通写法 |
- 跟for一样… go的if可以在条件之前加一个简单语句, 同样不推荐这样做
- 这个语句定义的
v作用范围仅在if范围之内. (如果有else 也在else中可以使用)
swtich-case 除了同样不需要括号, 允许判断条件快速初始化外, 也稍有点特别:
- 它匹配到一个条件, 会自动break, 无需你去手动编写
- 支持多个条件写一起
case a,b,c, 觉得这个还不错. - 它有一个
fallthrough关键字, 意思是会穿透当前匹配, 除了执行自己外还执行下一个条件, 然后马上结束 (不建议使用) - 允许switch后不跟条件, 类似于
if-else-if-else的语法糖, 但是同样不太推荐这种混用.
1 | func main() { |
- 需要注意的是go中匹配成功就会自动停止. 而不需要你去break…
- 当然.switch也可以不要条件(那是?) ,答案是当做
if -then -else的简版使用….(表示不太习惯
2. defer
Go在流程控制中引入了一个自造的特别关键字defer, 被它修饰的语句会先被加载, 但是延迟到当前方法体所有函数调用完后才会被调用, 所以字面意思翻译是延迟, 多个defer遵循先进后出的规则.
简单理解它, 比较类似其他语言的finally{}设计, 使用defer 调用一些期望必须最后被调用的方法.而不用放在代码块的最后面, 有几个场景适合使用它 (注意勿滥用defer)
- 资源释放, 各种需要最后执行的
close()方法, 即使中间过程可能抛出异常 - panic异常的捕获, go中
recover()只能配合defer使用 - 配合匿名函数使用, 达到某些函数结束后执行的效果 (慎重)
1 | func main() { |
最后引入一个小练习, 一次性写好有一定思考量, 请勿套公式或看任何提示.
0x03. 小练习
用自己的思路实实现开根号函数, 已知给定
func Sqrt(x float64) float64 {}函数体, 补充具体内容最后观察结果和 math.Sqrt的相似度, 以及计算效率, 这题作为一个算法面试题也挺好的, 值得好好思考和优化
这题最简单的就是思考 x^1/2^ 计算机是如何求值的, 比如 根号5 ≈ 2.2xxxxx, 题目简化了思考, 直接让大家用牛顿法, 我觉得自己思考一步步改进反而好得到, 就像求1+2+....n 一样, 直接用高斯公式就没意义了.
题目要求:
- 给定一个正数(≥0), 尽可能精准输出它的开方结果, 比如0^1/2^ =0, 4^1/2^ =2, 8^1/2^ ≈ 2.82xxx
- 最多只计算10次, 耗时时间尽可能短
- 覆盖大部分测试用例.
思路1:
这应该是大部分人首先想到的思路, 核心是利用二分查找, 快速的逼近最靠近的值, 并且算法复杂度是O(logn), 性能很好.
如果简单的思考一下, 很快就能写出一个最简版的查找, 最后的答案假设为
result, 给定一个数x- 当
x > 1时,1 < result < x, 可以使用标准的二分 - 当
0 < x <= 1时, 会发现这个问题会更严重, 因为这个区间内, 取值范围又变化了, 应该是x < result < 1. - 当
x=0, 单独返回 (小于0视为异常略)
- 当
那这样似乎就有了三种情况, 写的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// V1版本: Assert x >= 0 (略)
func SqrtBinary(x float64) float64 {
if x == 0 {
return 0
}
var min, max float64
if x > 1 {
min = 1
max = x
} else {
min = x
max = 1.0
}
var mid float64;
for i := 0; i < 10; i++ {
mid = (min + max) / 2
tmp := mid * mid
Println(mid, tmp)
if tmp > x {
max = mid
} else if tmp < x {
min = mid
} else {
return mid
}
}
return mid
}
func main() {
// x = 4 / 9, we should get 2/3
Println("\nFinal result =", SqrtBinary(4))
}但是上面这样的写法会发现有三个问题
- 输入4, 输出是2.0004.., 而不是2 ( 因为只要第一次尝试后mid变为小数, 之后的二分也就都是小数了)
- 我们需要针对0, 0
1, 1x三个范围来区分, 未免有点冗余, 思考一下如何统一 - 判断的条件较粗, 比较的时候应该用
tmp - x的绝对值和上一次对比, 会更高效一些
对上面三个问题我的思路是
- 根本原因是精度不够, 但是我想到一个比较取巧的办法, 当最后一次尝试后, 取result的四舍五入的平方和x进行对比, 如果正好相等则返回整数, 否则返回小数
- 这个问题关键是在于开始假设
0~x的范畴过简单, 没有考虑<1的问题, 那不妨试试0~x+1, 这样损失一点精度, 但可以一次性把条件包含在内 - 使用
abs(tmp-x)函数和上一次的结果做对比, 发现误差已经不能更小, 则跳出循环返回mid.
这里为了不引入新的包, 就只更新第二个问题, 并补充牛顿法(纯数学推导)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// V2版本: Assert x >= 0 (略)
func SqrtBinary(x float64) float64 {
min := 0.0
max := x + 1.0
var mid float64;
for i := 0; i < 20; i++ {
mid = (min + max) / 2
tmp := mid * mid
//Println(mid, tmp)
if tmp > x {
max = mid
} else if tmp < x {
min = mid
} else {
return mid
}
}
return mid
}
// V3版本: 牛顿/泰勒展开数学公式版本, 也许是标答之一
// 也可以简化为 a = (a^2 + x) / 2a (但不体现思维)
func Sqrt(x float64) float64 {
result := 1.0
for i := 0; i < 10; i++ {
result = result - (result*result-x)/(2*result)
Println(i, result)
}
return result
}
篇幅原因, 本篇介绍了最基本的语法和类型, 第二天学习剩余的结构和语言特性, 第三天学习一下Go的并发设计和运用, 第四天学习一下内存模型. 差不多入门篇就可以画上句号了.
参考资料:

