点开我就当至少你会Python C++ C# Java任意一种了,所以示例代码为主,没有的话不要继续看因为会看不懂。所以安装我就不多说了,Windows去这里下安装包,linux用包管理器。看完本文你将速通Go的基本语法,变量声明,数组,循环,Switch,函数,结构体,Map,错误处理,接口/鸭子类型,指针
先跑起来
创建个新文件夹,然后用vscode打开,搭一下脚手架
1 | go env -w GO111MODULE=on |
GO111MODULE?
GO111MODULE=on是开启 Go 的现代包管理模式,并且把这个设置全局保存
在 Go 1.11 版本(也就是变量名里 111 的由来)之前,Go 的包管理非常反人类,被称为 GOPATH 模式。
旧模式 (GOPATH):
所有的代码项目,必须强制放在一个固定的文件夹里(比如 D:\GoWorks\src)。这就像 C# 强制要求把所有 Visual Studio 的项目都放在C:\Windows\Microsoft.NET\Framework 下面一样荒谬。没有版本控制的概念。go get 下来的库,作者升级了,项目可能就炸了。
新模式 (Go Modules):
也就是 GO111MODULE=on。代码可以放在电脑的任何地方(桌面、D盘、E盘随便)。它使用 go.mod 文件来记录依赖版本(类似于 Python 的 requirements.txt 或 C# 的 .csproj/packages.config)。
项目? go install和go get?
如果不创建一个项目的话,vscode右下角会一直爆警告,而且代码补全可能会失效,因为它不知道包依赖关系。刚刚装的gopls就是go语言的IntelliSense引擎。项目也是使用go get安装第三方库要用的,类似于nodejs的package.json
go install会不会就是类似于python的pip install?其实不然
go install是用来装 工具(可执行程序 .exe)的,go get是用来装库(依赖包) 的。
Python的pip是两个都可以装,比如我可以pip install jupyter,然后直接在cmd里使用jupyter notebook启动网页端程序,也可以使用pip install requests装包然后在别的.py里使用
go把它拆开来了
比如刚刚装的gopls,实际就是:先下载gopls的源代码,把它编译成exe后丢进GOPATH里给vscode用
现在的 go get 已经禁止安装可执行文件了,必须用 go install。
| 命令 | 作用 | 产物 | 类比 Python |
|---|---|---|---|
go install |
安装二进制工具 | 生成 .exe 放到 bin 目录 |
pip install httpie (为了用命令) |
go get |
安装开发依赖库 | 更新 go.mod,下载源码供 import |
pip install requests (为了写代码) |
你好世界
编程惯例写个hello world能提高运气
1 | package main |
使用go build *.go生成二进制文件或者go run *.go直接运行
1 | PS D:\go-start> go build .\hello.go |
package main 是干嘛的?
Go 的编译器(go build 或 go run)需要知道代码的入口在哪里。
一个可执行程序必须有一个名为 main 的包,并且该包里必须包含一个 main() 函数。
如果没有 package main,Go 会把它当成一个库,就不会去找入口函数,也就跑不起来。
别的如果有编程基础都能看懂
变量声明
标准格式
1 | var hajimi string |
变量会被初始化为该类型的零值,也可以在后面加上赋值
短变量声明
1 | func main() { |
使用:=关键字,不需要写类型,go会直接根据内容推导出变量数据类型
但是注意,这样声明变量只能在函数内部。而且golang有个特性,函数内部声明了的变量就必须要用,不然编译会报错。所以最后把wochao赋值给了下划线 _ 占位符
匿名变量
1 | // 假设函数返回 (int, error) |
如果调用一个返回多个值的函数,但只想用其中一个,可以用下划线 _ 占位。
一次声明多个变量
1 | func main() { |
可以通过标准格式和短变量声明一次声明多个变量,变量可以是不同的数据类型
这样运行的输出是:
1 | 关小雨 18 |
批量声明
如果有很多很多变量,可以用括号包起来,通常用于全局变量
1 | var ( |
数组
Array
array,顾名思义,数组,和C一样,长度固定的数组
最经典
长度是类型的一部分,且长度固定。下面是最标准的写法,声明时指定长度和类型
1 | // 声明一个长度为 5 的 int 数组,默认全是 0 |
懒得数了
我懒得数长度了:
1 | q := [...]int{1, 2, 3, 4, 5} |
这样编译器会去数个数,长度还是5
指定赋值
可以只给数组里某几个位置赋值,其他位置会自动补零值。
1 | // 索引 1 是 10,索引 3 是 30,长度为 5 |
多维数组
用法和别的编程语言一样
1 | // 2行3列 |
Slice
OK上面关于数组的内容统统没有用,因为我们99%的时间都在用切片,而不是数组。切片是对数组的封装,它的长度可以根据需要动态增长。说人话就是动态数组
创建动态数组
和数组一样,就是不需要指定中括号里的数字
1 | s1 := []int{1, 2, 3} |
也可以使用make()并指定预留的长度和初始的数据
上面第二行的意思就是s2有5个0在里面,但是给他预留了10个int的空间
但是切片是动态长度,预留空间何意味?
如果不预留,超出长度的时候,Go 发现没地方了 -> 开辟新内存 -> 搬家 -> 再append ->
又没地方了 -> 再次开辟更大内存 -> 再次搬家
频繁搬家(内存分配和数据复制)非常消耗性能,前 10 次添加数据,Go 发现还有空位,直接放进去就行,不需要搬家。等到第 11 个数据来了,包间满了,Go 才会进行一次大规模搬家(通常会把容量翻倍,比如换个 20 人的大包间)。
设定 cap (容量) 是为了 减少底层数组扩容的次数,提高程序运行速度。虽然不设也能跑,但设了跑得更快。
往里面加东西
通过append可以往动态数组里塞数据
1 | s := []int{1, 2} |
动态数组的切片
和python的list切片类似,可以从一个现成的数组或动态数组中,截取出一部分。
1 | arr := [5]int{0, 1, 2, 3, 4} |
(所以我要叫它动态数组,要不然我就要说是切片的切片)
数组和动态数组的传递
数组是值传递,会把值复制一份用于传递。
1 | func change(a [3]int) { |
而切片是“引用”传递,实际上是传了一个包含指针的结构体。
1 | func update(s []int) { |
严格来说,Go 只有值传递。切片本质上是一个只有 3 个字段的结构体 Struct { ptr, len, cap }。当传递切片时,是把这个小结构体复制(值传递)了一份。但因为里面那个 ptr 指针指向了同一个底层数组,所以修改数据会同步。
循环
最经典
1 | for i := 0; i < 5; i++ { |
源·while
go里面没有while,但是可以直接把for循环当while用。
也是因为go没有while关键字,所以while在go里甚至可以定义成变量
1 | while := 0 |
如果什么都不加,直接一个for起手那就是死循环了
次跑循环
类似于python中的for i in range(5),不过格式是这么写的
1 | for i := range 5 { |
精准定位
类似于python的
1 | for i, char in enumerate("你好Go"): |
在go里面不需要借助额外的函数,直接这么写:
1 | for i, char := range "你好Go" { |
如果我要遍历数组或者动态数组
1 | for i, v := range nums |
就当go会展开为
1 | for i := 0; i < len(nums); i++ { |
go的for循环语法糖:
| 写法 | 含义 |
|---|---|
for i := range s |
只要索引 |
for _, v := range s |
只要值 |
for range s |
什么都不要,只循环次数 |
打个坐标
如果有嵌套循环,想在内层循环直接跳出最外层,可以使用标签。
1 | OuterLoop: |
不要把break OuterLoop理解为goto OuterLoop,标签只是为了指明作用范围。当执行 break OuterLoop 时,编译器会找到 OuterLoop 标记的那层循环,并立即终止这整层循环。
如果是想跳过当前剩余逻辑,直接进入外层循环的下一次迭代,那就和其他语言一样使用continue
Switch
最经典
1 | func main() { |
一case多值
我也不知道是不是go的特性
1 | switch x { |
也可以这么写,如果switch后面不加变量名,就类似于if-else
1 | switch { |
大炮穿箱
可以使用fallthrough来执行完当前 case 后继续执行下一个 case
1 | func main() { |
因为在case x > 2 && x < 10:增加了fallthrough,所以下一个default case也被执行了
1 | x 大于 2 小于 10 |
顺手的事
可以在 switch 后面定义一个只在 switch 块内有效的变量。
1 | func main() { |
函数
最经典
Go 的参数名在前,类型在后。
1 | // 单参数,单返回值 |
味大无需多盐,有其他编程基础的都能看懂
指定返回
在函数头定义返回变量名,函数体里赋值,最后只用写个return就能返回指定变量
1 | func getRect(width, height int) (area, perimeter int) { |
变长参数
用 ... 表示可以传入任意数量的参数。
1 | func sum(nums ...int) int { |
匿名函数
没有名字的函数,通常用于临时逻辑。
1 | func main() { |
方法函数
Go没有类,但可以给结构体绑定函数。这个马上讲结构体会再提这里不展开
1 | type User struct { |
结构体
Go 没有 class(类),结构体就是 Go 里的“类”。
它不仅能存数据,还能绑定方法,甚至通过“组合”来实现类似继承的功能。
创建结构体
使用 type 和 struct 关键字。用法和别的编程语言类似
1 | type user struct { |
上面的输出就是
1 | 关小雨 female 18 |
给结构体加函数
就是上文提到的方法函数(Method), Go 的方法定义和 C++ 不一样,它不写在结构体里面,而是写在外面,通过接收者 (Receiver)绑定到结构体上。
格式是这样:
1 | func (接收者变量 接收者类型) 方法名(参数) 返回值 { ... } |
接收者有两种,值接收者或者指针接收者
(u user):值接收者。相当于把结构体复制了一份传进来。改了它,原来的数据不会变。
(u *user):指针接收者,传的是内存地址。改了他原来的数据就变了
还是那个user结构体,给他分别用值接收者和指针接收者加上两个函数
1 | type user struct { |
这样的输出结果是
1 | 你好,我是 关小雨 |
嵌套/组合
既然没有继承,怎么复用代码?go提倡 “组合优于继承”。可以把一个结构体塞进另一个结构体里,这叫匿名嵌入。
1 | type Animal struct { |
这样的输出结果是
1 | 犬科 正在吃东西... |
Dog 并没有成为 Animal 的子类,它只是包含了一个 Animal。
Map
go语言的 map 就是哈希表,它是无序(每次遍历 Map 的顺序可能都不一样,Go 故意引入了随机性)的键值对集合。说人话就是python的dict和java的hashmap
声明与初始化
声明 map 后,如果不用 make 初始化,它就是 nil,直接赋值会报错 panic
可以这样用make创建:make(map[Key类型]Value类型)
1 | age := make(map[string]int) |
也可以通过字面量初始化,声明的时候赋值
1 | age := map[string]int{ |
增删改查
无需多盐,字面意思
1 | m := make(map[string]string) |
查不到啊
这是 map 独特的特性。去拿一个不存在的 Key 时,不会报错,而是返回该类型的零值(比如 int 返回 0,string 返回 “”)。
但这有个坑:不知道返回的 0 是因为它真的存了 0,还是因为根本没这个 Key。
解决方案:双返回值判断
1 | func main() { |
ok 是一个布尔值,true 表示存在,false 表示不存在。
Key能用的类型?
只要能用 == 比较的类型,都能当 Key。
可以:bool, int, float, string, 指针, channel, 接口
不可以:动态数组, map, 函数
错误处理
众所不周知,Go 语言没有 try-catch-finally 这种异常处理机制。
Go 的设计哲学是:错误也是一种值。它不希望把错误“抛出”到一个不可预知的地方,而是希望原地处理或者显式地传递它。
在继续错误处理之前,得知道这么一个go的关键字:nil
nil
nil 是 go 语言中某些特定类型的默认零值。可以把它类比为其他语言中的 null 或 None
在 Go 中,这些类型的“空值”都是 nil:接口,指针 ,切片,映射,通道和函数。
但是go的基本类型不能是nil而是默认值:
int 的零值是 0。
string 的零值是 ""(空字符串),不是 nil。
bool 的零值是 false。
这样就杜绝了像 C 语言中“这到底是数字 0 还是空指针”的二义性。
多返回值
让函数在返回结果的同时,多返回一个 error 类型的值。
1 | func doSomething() (int, error) { |
如果返回的error不是nil,那就出大事了
添加信息
给底层的错误加点描述,再往上传,可以使用 %w。
1 | func openConfig(filepath string) error { |
这样的输出就是
1 | 出事了 读取配置文件失败: open config.yaml: The system cannot find the file specified. |
扣1复活
Go 虽然没有 try-catch,但有 panic-recover 机制。这不是用来处理普通业务错误的,而是用来处理毁灭性错误(比如数组越界、空指针、数据库连接不上)。
1 | func protect() { |
defer这个关键字的意思是:它后面的函数在当前函数退出前的最后一刻,一定会执行。哪怕函数崩溃了,defer 里的内容也会被执行。
recover是用来”复活”程序的,只能写在defer里。用于获取到出的问题。recover 不仅能捕获手动写的 panic,还能捕获go运行时触发的“运行时错误”(Runtime Error)。
panic是用来手动触发一个错误的函数
这样的执行结果是:
1 | 准备开始搞破坏... |
自定义error
在 Go 中,任何实现了Error() string方法的类型都可以是一个错误。
1 | type MyError struct { |
这个后文讲鸭子类型的时候会再翻出来解析
接口/鸭子类型
如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。
在 Go 里:只要一个类型实现了接口要求的所有方法,它就自动属于这个接口,不需要显式写 implements。
创建和使用
先这样定义一个鸭叫接口
1 | type Quack interface{ Quack() } |
但是不止鸭子能叫,人也能学鸭子叫。所以再定义人和鸭的结构体并实现Quack函数
1 | type duck struct{ name string } |
现在再来创建一个用于调用鸭叫接口的函数
1 | func makeQuack(q Quack) { q.Quack() } |
person和duck都可以调用这个鸭叫接口函数
1 | func main() { |
输出是:
1 | 唐老鸭 嘎嘎嘎 |
空接口
go中有一个特殊的接口:any,没有定义任何方法。既然没有任何要求,那就意味着:任何类型都实现了空接口。它可以装万物
1 | type duck struct{ name string } |
这样的输出结果是:
1 | 10 |
指针
Go 的指针在语法上和 C/C++ 非常像,但在逻辑上被“阉割”,为了安全和简单。
没有指针运算,没有复杂的内存管理,没有 -> 箭头符号
使用指针
和C/C++一样。& (取地址),获取变量的内存地址。* (解引用),获取指针指向的值,或者定义指针类型。
1 | func main() { |
结构体指针的“语法糖”
在 C++ 中,访问指针对象的字段要用箭头 p->age。在 Go 中,统一都用点 .。编译器会分辨是值还是指针。
1 | type User struct { |
使用场景
需要修改外部变量
go函数默认是值传递(拷贝)。如果想在函数里修改外面的变量,必须传指针。
1 | // 接收指针类型 *int |
避免大对象拷贝
如果结构体很大(比如有 100 个字段),传值会发生一次巨大的内存拷贝。传指针只需要拷贝 8 个字节(64位系统),速度快很多。
总而言之
| 特性 | C++ 指针 | Go 指针 |
|---|---|---|
| 定义符号 | * |
* |
| 取址符号 | & |
& |
| 成员访问 | -> (指针), . (对象) |
统一用 . |
| 指针运算 | 支持 (p++, p+1) |
不支持 (报错) |
| 野指针/释放 | 需要手动 delete |
GC 自动回收,不用管 |
| 安全性 | 危险 (易越界/内存泄漏) | 相对安全 |
public?private?
虽然我觉得这么做有点逆天但是,在 Go 中,首字母大写类似于Public,首字母小写类似于Private。不仅仅是变量名,还包括函数名、结构体名、结构体内部的字段名
为什么说类似,因为这个public和private都指的是能不能跨包访问
1 | package main |
这样运行,输出是空的,json.Marshal 是 json 包里的函数,它是“外人”,它看不到 User 里没导出的 name 和 age。
但是如果把结构体变量名首字母都改成大写,就正常了
1 | package main |
运行正常输出
1 | {"Name":"关小雨","Age":18} |
Congratulations
恭喜,看完本文你已经了解了其他编程语言到golang的迁移
你可能在找 Goroutine Channel?Go 最强的并发特性留到下一篇专门讲。