Go语言学习
本文最后更新于:2023年6月18日 下午
Go语言学习
本笔记基于菜鸟教程Go语言教程和Go语言编程编写,详情请点击Go语言教程和Go语言之旅。
第一个Go语言教程
hello.go
文件
1 |
|
运行go语言代码。
1 |
|
编译生成二进制文件
1 |
|
Go语言的基础组成部分有以下几个部分;
包声明
hello.go
文件中package main
定义了包名,你必须在源文件中非注释的第一行指明这个文件属于哪个包,package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为main
的包。引入包
import "fmt"
表明导入了fmt包,跟python一致,fmt 包实现了格式化 IO(输入/输出)的函数。通过 import 关键字来导入其他非 main 包。
可以通过 import 关键字单个导入:
1
2import "fmt"
import "io"也可以同时导入多个:
1
2
3
4import (
"fmt"
"math"
)省略调用(不建议使用):
1
2// 调用的时候只需要Println(),而不需要fmt.Println()
import . "fmt"函数
func main()
是程序开始执行的函数。main
函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。变量&语句&表达式
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
注释
单行注释
//
,多行注释/*
开头,*/
结尾,与c语言一致。
Go语言基础语法
行标识符
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。
字符串拼接
Go语言字符串拼接使用+
实现:
1 |
|
可见性规则
Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。
函数名首字母小写即为 private :
1 |
|
函数名首字母大写即为 public :
1 |
|
Go fmt包(输入输出)
输出相关函数
Print()
函数将参数列表 a 中的各个参数转换为字符串并写入到标准输出中。
非字符串参数之间会添加空格,返回写入的字节数。
1 |
|
Println()
函数功能类似 Print,只不过最后会添加一个换行符。
所有参数之间会添加空格,返回写入的字节数。
1 |
|
Printf()
函数将参数列表 a 填写到格式字符串 format 的占位符中。
填写后的结果写入到标准输出中,返回写入的字节数。
1 |
|
以下三种与上面三种类似,不过最终会返回输出的字符串,并不会打印
1 |
|
以下函数功能同 Sprintf() 函数,只不过结果字符串被包装成了 error 类型。
1 |
|
示例
1 |
|
输入相关函数
Scan()
从标准输入中读取数据,并将数据用空白分割并解析后存入 a 提供的变量中(换行符会被当作空白处理),变量必须以指针传入。
当读到 EOF 或所有变量都填写完毕则停止扫描。
返回成功解析的参数数量。
1 |
|
Scanln
和 Scan
类似,只不过遇到换行符就停止扫描。
1 |
|
Scanf
从标准输入中读取数据,并根据格式字符串 format 对数据进行解析,将解析结果存入参数 a 所提供的变量中,变量必须以指针传入。
输入端的换行符必须和 format 中的换行符相对应(如果格式字符串中有换行符,则输入端必须输入相应的换行符)。
占位符 %c 总是匹配下一个字符,包括空白,比如空格符、制表符、换行符。
返回成功解析的参数数量。
1 |
|
以下三个函数功能同上面三个函数,只不过从 r 中读取数据。
1 |
|
以下三个函数功能同上面三个函数,只不过从 str 中读取数据。
1 |
|
示例
1 |
|
Go语言数据结构
布尔型
1 |
|
示例
1 |
|
数字类型
1 |
|
从Go1.9版本开始,对于数字类型,无需定义int及float32、float64,系统会自动识别。
1 |
|
派生类型
1 |
|
Go安装依赖包
安装完成Golang之后,我们可以使用go env
,查看基本配置信息
1 |
|
Go get
go get 命令可以借助代码管理工具通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装。整个过程就像安装一个 App 一样简单。
使用go get命令下载指定版本的依赖包
执行go get
命令,在下载依赖包的同时还可以指定依赖包的版本。
- 运行
go get -u
命令会将项目中的包升级到最新的次要版本或者修订版本; - 运行
go get -u=patch
命令会将项目中的包升级到最新的修订版本; - 运行
go get [包名]@[版本号]
命令会下载对应包的指定版本或者将对应包升级到指定的版本。
Go module
go module是go官方自带的go依赖管理库,在1.13版本正式推荐使用
go module可以将某个项目(文件夹)下的所有依赖整理成一个 go.mod 文件,里面写入了依赖的版本等
使用go module之后我们可不用将代码放置在src
下了
开启Go module
go在1.13版本默认是auto,1.13+的版本判断开不开启MODULE的依据是根目录下有没有go.mod文件。
我们也可手动更改为 on(全部开启)/off(全部不开启)
1 |
|
Go module基本命令
初始化
在创建一个新的Golang项目时,我们可能没有创建go.mod
文件,因此我们可以使用下面命令初始化一个module,模块名为你的项目名。
1 |
|
可以看到已经初始化了go.mod
,只是内部没有任何依赖信息。
更新依赖
1 |
|
tidy
会检测该文件夹目录下所有引入的依赖,并写入go.mod
,不使用或错误的依赖也会自动进行删除。
下载依赖
1 |
|
目前所有模块版本数据均缓存在$GOPATH/pkg/mod
和 $GOPATH/pkg/sum
下,同时会在项目根目录下生成 go.sum 文件, 该文件是依赖的详细依赖。
导入依赖
1 |
|
我们的依赖下载完成后是存储了$GOPATH
目录下的,如果有特殊需要,需要将GOPATH
下的依赖转移至该项目根目录下的 vendor(自动新建) 文件夹下,可以使用该命令。
校验依赖
1 |
|
参考链接
Go语言基础知识
包
每个 Go 程序都是由包构成的。
程序默认从main
包开始运行。
按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand"
包中的源码均以 package rand
语句开始。
1 |
|
导入
Go语言与python类似,都是能够导入其他程序包,导入方式一般有两种,分组导入或多个导入语句
1 |
|
导出名
在Go中,以大写字母开头,那么它就是已导出的。已导出表示能够在包外访问,Go语言中调用其他包的函数都是已导出的函数。
函数
函数类型声明
go语言的函数声明与C语言函数声明类似,函数可以没有参数或接受多个参数,但是与c语言不同的是,函数参数类型说明位置不同,关于Go语言为什么采用这种声明方式,可以参考这篇文章,总而言之Go 的声明从左到右阅读,C 的声明是螺旋式的。
1 |
|
go语言中函数类型是可以省略的,当连续两个或多个函数的已命名形参类型相同时,除最后一个类型外,其他都可以省略。上述例子简写为:
1 |
|
多值返回
go语言中的函数能够返回任意数量的返回值,这是c语言所不具有的。
1 |
|
命名返回值
Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。没有参数的 return
语句返回已命名的返回值。也就是 直接
返回。
1 |
|
变量
var
语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。
1 |
|
同时变量也可以进行初始化操作,每个变量对应一个。如果初始化值存在,可以省略类型;变量会从初始值中获取类型。
1 |
|
短变量
在函数中,简洁赋值语句 :=
可在类型明确的地方代替 var
声明。函数外的每个语句都必须以关键字开始(var
, func
等等),因此 :=
结构不能在函数外使用。
零值
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
- 数值类型为
0
, - 布尔类型为
false
, - 字符串为
""
(空字符串)
类型转换
Go语言类型转换是显式类型转换,表达式 T(v)
将值 v
转换为类型 T
。
一些关于数值的转换:
1 |
|
常量
常量的声明与变量类似,只不过是使用 const
关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 :=
语法声明。
1 |
|
iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
1 |
|
循环结构
Go语言中只有一种循环结构:for
循环。对你没看错,只有这一种,while
循环都是通过for
变形而来。
基本的 for
循环由三部分组成,它们用分号隔开:
- 初始化语句:在第一次迭代前执行
- 条件表达式:在每次迭代前求值
- 后置语句:在每次迭代的结尾执行
注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { }
则是必须的。
1 |
|
Go中的while循环
1 |
|
条件控制语句
Go 的 if
语句与 for
循环类似,表达式外无需小括号 ( )
,而大括号 { }
则是必须的.
1 |
|
if
语句同样可以在条件表达式前执行一个简单的语句,比如声明变量,该变量的作用域仅在if
和else
之内。
switch控制语句
Go语言中的switch语句,运行时会从上向下顺次执行,直到匹配成功时停止。,Go语言对每个case默认提供了break
语句,使用 fallthrough
会强制执行后面的 case 语句,fallthrough
不会判断下一条 case 的表达式结果是否为 true。
select语句
select 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
select语句的语法:
- 每个 case 都必须是一个通道
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通道可以进行,它就执行,其他被忽略。
- 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。在关闭文件或者网络接口是极其有用。
defer
有一下规则需要注意:
- 推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
1 |
|
- 如果
defer
执行的函数存在参数,那么该参数就是执行defer
时值,并不会因为后续的操作而变化。例如:
1 |
|
defer
可以修改调用函数的命名返回值。
1 |
|
return
首先会对返回值进行值拷贝,将return
语句返回的值复制到函数返回值栈区(如果只有return
,不带任何变 或值,则此步骤不做任何动作)。1
2
3
4
5
6func d() (i int) {
t := 3
defer func() { t++ }()
return t
}
// c() return 3
实际上,defer
语句是本函数return
语句进行调用的,举个例子:
1 |
|
实际上执行流程为
1 |
|
defer函数的参数值,是在申明defer时确定下来的。在defer函数申明时,对外部变量的引用是有两种方式:作为函数参数和作为闭包引用
- 作为函数参数,在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算
- 而作为闭包引用,在defer函数执行时根据整个上下文确定当前的值
举个比较特殊的例子
1 |
|
panic
内置函数,能够直接终止程序控制流,同时调用本函数defer
,并将panic
向上层传递,直到整个程序返回异常。
Recover
终止panic
的函数,一般写在defer
中,能够终止panic
的终止信号,并停止向上层传递,此时程序将正常返回。
1 |
|
recover都是在
当前的goroutine
里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的
数据类型
指针
Go语言中存在指针,保存值的内存地址,指针的操作与C语言中的类似,但是,Go中默认没有指针运算,即不支持++/--
操作。
1 |
|
当然,如果你真的想用指针运算的话,可以使用uintptr
实现,uintptr
支持指针运算,只不过普通指针需要需要先转换为unsafe.Pointer
,然后在转换为uintptr
(没有uintptr
与普通指针的转换方式)。
1 |
|
结构体
Go语言结构体声明和使用与C语言类似,使用struc_name.filed_name
访问。
1 |
|
值得注意的是,如果使用结构体指针,Go语言中允许我们使用隐式间接引用
struc_ptr.filed_name
,而并非C语言的struc_ptr->filed_name
形式。
Go语言结构题可以通过直接列出字段的值来新分配一个结果。
1 |
|
切片与数组
每个数组的大小都是固定的,而切片可以动态提取数组中的一部分,切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:
1 |
|
它会选择一个半开区间,包括第一个元素,但排除最后一个元素。切片拥有长度和容量,切片的长度它所包含的元素个数,使用len(s)
获取;切片的容量是从它的第一个元素(注意是切片选取的,并不是数组的)开始数,到其底层数组元素末尾的个数,使用cap(s)获取。
以下表达式创建了一个切片,它包含 a
中下标从 1 到 3 的元素:
1 |
|
切片的结构如下图所示,
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。但是,如果一个引用数组的切片进行了copy或者append等操作时,这个切片就与原先的数组脱离的联系,仅仅内容一致而已。
创建数组时我们需要指定数组的长度,而切片可以从已创建数组中截取,也可以新建一个数组然后引用:
1 |
|
函数闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。Go通常使用这种方式来保存函数内部的局部变量,这与C语言中的静态局部变量功能类似,但有所区别:
1 |
|
在上述程序中pos
和neg
所形成的闭包是不同的,因此其内部的局部变量也是不同的,C语言中无论如何调用函数,静态局部变量始终是同一个。
斐波那契闭包
1 |
|
方法
Go中不存在类的概念,但是可以为结构体定义方法。方法就是一类带特殊接收者参数的函数(可以理解为数据类型的类函数)。方法接收者在它自己的参数列表内,位于 func
关键字和方法名之间。Abs
方法拥有一个名为 v
,类型为 Vertex
的接收者。
1 |
|
注意,方法的声明只能为同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括
int
之类的内建类型)的接收者声明方法。如果需要声明其他包类型,可以在本包内使用
type
重新声明一个类型别名
当然由于Go语言函数值是值传递,因此,如果我们需要在方法内修改接收者的内容,则传递传递指针,即为指针接收者声明方法。另外,Go语言为了方便起见,变量和指向变量的指针是隐式转换的,我们都可以通过v.fun()
直接访问该类型的方法。当然如果是普通函数传参的话,指针和变量无法隐式转换,是会报编译错误,望周知。
1 |
|
接口
接口类型是有一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值。如果某个类型实现了接口中的所有方法,我们就说该类型实现了该接口。个人理解就是接口是Go语言中类似于多态的一种机制,接口类型就是虚基类。
与方法不同的是,如果我们使用接口类型多态执行相关函数,需要严格判断参数的类型,此时指针类型与变量类型无法被认为是同一种。
接口也是值,它们可以像其他值一样传递。接口的内部类型可以简单理解为value
和type
的结构体(实际上接口类型更加复杂,但是在作用上类似于类型和值),接口的实现就是通过type
找到相应数据的底层类型,调用其底层类型的同名方法。
接口类型断言
类型断言提供了访问接口值底层具体值的方式。
1 |
|
该语句断言(确信)接口值 i
保存了具体类型 T
,并将其底层类型为 T
的值赋予变量 t
。若 i
并未保存 T
类型的值,该语句就会触发一个panic
。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
1 |
|
若 i
保存了一个 T
,那么 t
将会是其底层值,而 ok
为 true
;否则,ok
将为 false
而 t
将为 T
类型的零值,程序并不会产生恐慌。
接口类型查询
接口类型查询用于查询一个接口变量底层的具体类型是什么,或者该接口变量是否实现了其他接口。类型选择中的声明与类型断言 i.(T)
的语法相同,只是具体类型 T
被替换成了关键字 type
。
1 |
|
此选择语句判断接口值 i
保存的值类型是 T
还是 S
。在 T
或 S
的情况下,变量 v
会分别按 T
或 S
类型保存 i
拥有的值。在默认(即没有匹配)的情况下,变量 v
与 i
的接口类型和值相同。
Stringer接口
1 |
|
Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值(感觉像是php中的_toString()_方法(っ °Д °;)っ)。
1 |
|
error接口
Go 程序使用 error
值来表示错误状态。
1 |
|
通常函数会返回一个 error
值,调用的它的代码应当判断这个错误是否等于 nil
来进行错误处理。
1 |
|
go语言并发
信道
信道是带有类型的管道,使用<-
来发送和接受数据。
Channel
类型定义
1 |
|
1 |
|
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
信道也可以设置缓冲区,在这种情况下,仅当缓冲区填满后,发送方才会阻塞;仅当缓冲区为空时,接收方才会阻塞。
1 |
|
信道默认情况下不需要显示的进行关闭但是在某些情况下可能需要表示数据传输完成,可以使用close(ch)
关闭指定信道,此时,下列ok
属性会被职位false
,代表信道关闭,不能继续传入数据。循环 for i := range ch
会不断从信道接收值,直到它被关闭。
1 |
|
select语句
select
语句使一个 Go 程可以等待多个通信操作。
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
1 |
|
默认情况下,主函数读取信道c,case c<-x
分支即可继续,case <-quit
一直阻塞;当执行到quit <- 0
,信道c阻塞,信道quit
存入数据,case c<-x
分支阻塞,case <-quit
执行继续。
select中也存在默认分支,当其他分支都没有准备好时,
default
分支就会执行。
练习
判断等价二叉查找树
1 |
|
web并发爬虫
1 |
|
Go并发
GOMAXPROCS
GOMAXPROCS
是 Golang 提供的非常重要的一个环境变量设定。通过设定 GOMAXPROCS
,用户可以调整 Runtime Scheduler 中 Processor(简称P)的数量。Processor
是类似于 CPU 核心的概念,其用来控制并发的 M 数量。
1 |
|
func Goexit
func Goexit
用于结束当前goroutime
的运行, Go exit
在结束当前 goroutine
运行之前会调用当前 goroutine
已经注册的 defer
,Goexit
并不会产生 panic
,所以该 goroutine defer
里面的 recover
调用都返回 ni
。
func Gosched
func Gosched
是放弃当前调度执行机会,将当前 goroutine
放到队列中等待下次被调度。