golang基础汇总(二), 对函数、结构体、接口、错误处理、 工具链 使用详解。
函数
函数类型 其实就是一个指针
函数名本身就是一个指针类型数据 在内存中代码区进行存储
函数定义
1 func 函数名(参数名 参数类型...) [ [return_name] return_type ...]{
2 函数体
3}
函数支持可变参数
1//函数支持可变参数
2func getsum2(args ...int) int{
3 sum :=0;
4 for _,val :=range args {
5 sum +=val
6 }
7 return sum
8}
9
10func main(){
11 z :=getsum2(1,2,3,4,5,6,7,8,9,10) //1+2+3...+10 ,//函数支持可变参数
12 fmt.Println(z) //55
13}
多返回值
1//多返回值
2func returnmany1(a ,b int) (int,int){
3 c :=a+b
4 d :=3
5 return c,d
6}
7
8
9 // 多返回值,指定名字,可以直接return
10func returnmany2(a int ,b int) (c int,d int) {
11 c=a+b
12 d=3
13 return
14}
函数作为形式参数
1 // 函数是一种数据类型,也可以作为形式参数
2 func myfunc(getsum func(int,int) int, a,b int) int{
3 return getsum(a,b)
4 }
5func getsum(a int ,b int) int{
6 return a+b
7}
8func main(){
9 f :=myfunc(getsum,3,4) //函数名作为实际参数
10 fmt.Println(f) //7
11}
定义函数类型
1// 自定义类型
2type myfunctype func(int,int) int
3
4func getsum(a int ,b int) int{
5 return a+b
6}
7
8func myfunc2(getsum myfunctype,a int,b int) int {
9 return getsum(a,b)
10}
11t :=myfunc2(getsum,3,4) //自定义类型
12fmt.Println(t)
1
2package main
3
4import "fmt"
5
6func test6(){
7 fmt.Println("瓜娃子")
8}
9func test7(a int, b int){
10 fmt.Println(a+b)
11}
12func test9(a int, b int) int{
13 return a+b
14}
15func test8() {
16 fmt.Println("细伢子")
17}
18//type 可以定义函数类型
19//type 可以为已存在类型起别名
20type FUNCTYPE func()
21type FUNCTEST func(int,int)
22type funcdemo func(int,int)int
23
24
25func main1101() {
26 //定义函数类型变量
27 var f FUNCTYPE
28 f=test6
29 //通过函数变量调用函数
30 f()
31
32 //f=test7//err函数类型不一致
33 //f()
34 f=test8
35 f()
36
37 //函数类型 其实就是一个指针
38 var f1 FUNCTEST
39 f1=test7
40 f1(10,20)
41
42 var f2 funcdemo
43 f2=test9
44 v:=f2(10,20)
45 fmt.Println(v)
46}
47
48
49func test10(a int, b int){
50 fmt.Println(a+b)
51}
52
53func main(){
54 //函数调用
55 test10(10,20)
56 fmt.Printf("%p\n",test10)
57 //如果使用Print打印函数名是一个地址
58 //函数名本身就是一个指针类型数据 在内存中代码区进行存储
59 fmt.Println(test10)//地址
60
61 //自动类型推到创建函数类型
62 //f:=test10
63 //f(10,20)
64 //fmt.Printf("%T",f)
65 //直接定义函数类型
66 var f func(int,int)
67 f=test10
68 f(10,20)
69
70 var c func(int,int)=test10
71 c(10,10) //20
72}
函数数据作用域
全局变量 在函数外部定义的变量成为全局变量
全局变量作用域是项目中所有文件
全局变量在内存中数据区存储 和const定义的常量存储位置都是数据区
1package main
2import "fmt"
3
4//全局变量 在函数外部定义的变量成为全局变量
5//全局变量作用域是项目中所有文件
6//全局变量在内存中数据区存储 和const定义的常量存储位置都是数据区
7var a int=10//如果全局变量没有初始值值为0
8//全局常量
9const b int =10
10func demo2(){
11 //a=123
12 //const a int =10
13 fmt.Println(a) //10 局部找不到找全局
14}
15
16func main01() {
17 //局部变量 在函数内部定义的变量 作用域限定于本函数内部 从变量定义到本函数结束有效
18 //var 变量名 数据类型 先定义后使用
19 //在同一作用域范围内 变量名是唯一的
20 a:=10
21 //匿名内部函数
22 {
23 //var a int =20
24 a:=20
25 fmt.Println(a)
26 }
27 fmt.Println(a) //10
28
29 //程序中如果出现了相同的变量名 如果本函数有自己的变量 就使用自己的 如果没有上外层寻找变量
30 //如果名字相同会采用就近原则 使用自己的变量
31 i:=0
32 for i:=0;i<10;i++{
33
34 }
35 fmt.Println(i) //0
36
37}
38
39func main(){
40 demo2()
41 //采用就进原则使用局部变量
42 //在局部变量作用域范围内 全局变量不起作用
43 a:=234
44 fmt.Println(a)
45 main01()
46 {
47 fmt.Println(123) //123 直接输出
48 }
49 func(){
50 fmt.Println(123) //匿名函数自己调用 输出 123
51 }()
52
53 var c=10
54 d:=func(){
55 fmt.Println(c)
56 }
57 d() //输出10
58}
递归函数
递归需要有出口 必须待return关键字
1//计算n的阶乘
2var sum int = 1
3func test4(n int){
4 if n==1{
5 return
6 }
7 test4(n-1)
8 sum*=n
9}
10
11func main(){
12 test4(3)
13 fmt.Println(sum)
14}
init函数
init 函数的注意事项和细节
如果一个文件同时包含全局变量定义,init 函数和 main 函数, 则执行的流程
全局变量定义->init函数->main函数
main包 引入 util包 , 如果util 包 也有 全局变量 和init 函数 那么执行顺序是
main.go ->import util包-> util 的全局变量->util的 init函数->main中的全局变量定义->main中的init函数->main函数
匿名函数
匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序
1func main(){
2 f:=func(a int, b int)int{
3 return a+b
4 }
5 res1 :=f(10,20)
6 res2 :=f(20,30)
7 fmt.Println(res1,res2)
8}
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
1//闭包, 返回的是函数,在main函数栈区中,不销毁,直到main结束
2package main
3import "fmt"
4//函数在调用结束会从内存中销毁
5func test1(a int){
6 a++
7 fmt.Println(a)
8}
9
10func main01() {
11 a:=0
12 for i:=0;i<10 ;i++ {
13 test1(a)
14 }
15}
16
17//可以通过匿名函数和闭包 实现函数在栈区的本地化
18func test2() func() int{
19 var a int
20 return func() int{
21 a++
22 return a
23 }
24}
25func main(){
26 main01()
27 //将test2函数类型赋值给f
28 //函数调用将test2的返回值给f
29 f:=test2()
30 for i:=0;i<10 ;i++ {
31 fmt.Println(f())
32 }
33 fmt.Printf("%T",f)
34}
test2()返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 a ,因此这个匿名函数就和 a形成一 个整体,构成闭包。
可以这样理解: 闭包是类, 函数是操作,a是字段。函数和它使用到 a构成闭包。
闭包实践
1func makeSuffix(suffix string) func(string) string {
2 return func(name string) string {
3 if !strings.HasSuffix(name,suffix) {
4 return name + suffix
5 }
6 return name
7 }
8}
9
10f2 :=makeSuffix(".jpg")
11fmt.Println("文件名处理后=",f2("winter")) //winter.jpg
12fmt.Println("文件名处理后=",f2("bird.jpg")) //bird.jpg
返回的匿名函数和 makeSuffix(suffix string)的suffix变量组合成一个闭包,因为 返回的函数引用到suffix这个变量
我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复 使用。
函数的defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
通过defer方式 将函数信息加载到内存,并没有执行和释放,在函数出栈的时候,执行defer对应的内容
1package main
2import "fmt"
3func mysum(a int, b int) int {
4 //当执行defer时,展示不执行,压入导独立导栈 (defer栈)
5 //当函数执行完毕后,再从defer栈中,按照先入后出,执行
6 defer fmt.Println(a) //第三步输出10
7 defer fmt.Println(b) //第二步 输出20
8 c := a + b
9 fmt.Println(c) //第一步 输出30
10 return c
11}
12func main() {
13 c := mysum(10, 20)
14 fmt.Println(c) //第四步 输出30
15}
defer 的注意事项和细节
-
当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中, 然后继续执行函数下一个语句。
-
当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),
-
在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。
1package main
2import "fmt"
3func mysum(a int, b int) int {
4 //当执行defer时,展示不执行,压入导独立导栈 (defer栈)
5 //当函数执行完毕后,再从defer栈中,按照先入后出,执行
6 defer fmt.Println(a) //第三步输出10
7 defer fmt.Println(b) //第二步 输出20
8 a++ //11
9 b++ //21
10 c := a + b
11 fmt.Println(c) //第一步 输出32
12 return c
13}
14func main() {
15 c := mysum(10, 20)
16 fmt.Println(c) //第四步 输出32
17}
函数参数传递
两种传递方式
- 值传递
- 引用传递 (也是值copy,只不过是地址的copy)
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的
拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的 数据大小,数据越大,效率越低。
值类型和引用类型
1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
错误处理
error
类型其实是一个接口类型,也是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法Error
。这个方法不接受任何参数,但是会返回一个string
类型的结果。
defer recover
当程序发生错误时会panic,程序就会退出 , recover可以捕获异常,让程序继续执行
1package main
2
3import "fmt"
4
5func demo(i int) {
6 var arr [10]int
7 defer func() {
8 err := recover()
9 if err != nil {
10 fmt.Println(err)
11 }
12 }()
13
14 arr[i] = 10 //在寄存器中完成
15 fmt.Println(2) //不打印
16 fmt.Println(3) //不打印
17}
18
19func main() {
20 demo(10)
21 fmt.Println("1")
22}
23//遇到错误了,demo函数栈信息就开始返回了,清空demo函数栈中的内存
24
25//runtime error: index out of range [10] with length 10
26//1
自定义错误
go程序中,也支持自定义错误,使用errors.New和panic 内置函数
-
erros.New(“错误说明”) ,会返回一个error类型的值,表示一个错误
-
panic 内置函数 ,接收一个interface() 类型的值(也就是任意值了)作为参数,可以接受error类型的变量,输出错误信息,并退出程序。(运行时恐慌)
1package main
2
3import (
4 "fmt"
5 "github.com/pkg/errors"
6)
7func test1(){
8 err :=readConf("config2.ini")
9 if err !=nil {
10 panic(err)
11 }
12 fmt.Println("继续执行")
13
14}
15func readConf(name string) error{
16 if name == "config.ini" {
17 return nil
18 }
19 return errors.New("读取错误")
20}
21func main(){
22 test1()
23}
自定义异常
错误类型+error方法返回的错误类型值 实现
1
2package myexception
3
4import (
5 "fmt"
6 "time"
7)
8
9type InvalidRadiusError struct {
10 //非法半径
11 InValidRadius float64
12 //合法的最小半径
13 MinValidRadius float64
14 //合法的最大半径
15 MaxValidRadius float64
16 //异常的发生时间
17 errTime time.Time
18}
19
20func GetCircleArea(radius float64) (area float64, err error) {
21
22 if radius < 10 || radius > 50 {
23 return 0, NeWInvalidRadiusError(radius,10,50)
24 }
25
26 return 3.14 * radius * radius, nil
27}
28
29//实现error 接口
30func (ire *InvalidRadiusError) Error() string {
31 return ire.String()
32}
33//异常被打印的方式
34func (ire InvalidRadiusError)String() string {
35 return fmt.Sprintf("invalidRadius{%.2f是非法半径,合法半径[%.2f,%.2f],错误发生时间是%v}",ire.InValidRadius,ire.MinValidRadius,ire.MaxValidRadius,ire.errTime)
36}
37
38//创建自定义异常的工厂方法
39func NeWInvalidRadiusError(inValidRadius,minRadius,maxRadius float64) *InvalidRadiusError{
40 ire :=new(InvalidRadiusError)
41 ire.InValidRadius=inValidRadius
42 ire.MinValidRadius=minRadius
43 ire.MaxValidRadius=maxRadius
44 ire.errTime=time.Now()
45 return ire
46}
47
48func MyexceptionTest(){
49 area,err :=GetCircleArea(-5)
50 fmt.Println(err)
51 fmt.Println(area)
52}
从 panic被引发到程序终止运行的大致过程
先说一个大致的过程:某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。
这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。
这里的最外层函数指的是go
函数,对于主 goroutine 来说就是main
函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。
随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。
go的工具链
go build
标准格式
- go build [-o output] [-i] [build flags] [packages]
- -o 参数决定了编译后文件名称,例如我们要程序main.go编译后程序名为hello,我们可以执行以下命令
附加参数 | 备注 |
---|---|
-v | 编译时显示包名 |
-p n | 开启并发编译,默认情况下该值为 CPU 逻辑核数 |
-a | 强制重新构建 可以用来调试标准库 |
-n | 打印编译时会用到的所有命令,但不真正执行 |
-x | 打印编译时会用到的所有命令 (都执行了哪些操作。) |
-x -v 让构建过程一目了然
build -x -v 的输出,我们还看到 go build 过程主要调用了 go tool compile ($GOROOT/pkg/tool/linux_amd64/compile) 和 go tool link ($GOROOT/pkg/tool/linux_amd64/link) 分别进行包编译和最终的链接操作。
编译以及链接命令中的每一个标志选项都会对最终结果产生影响,
比如:-goversion 的值就会影响 go 编译器的行为,而这个 goversion 选项的值可能来自 go.mod 中的 go 版本指示标记,笔者就遇到过一次因 goversion 值版本过低而导致的问题。而 - v -x 选项对这类问题的解决是会起到关键作用的。
-a:强制重新构建所有包
-race: 让并发 bug 无处遁形
-gcflags:传给编译器 (compiler) 的标志位选项集合
1go build -gcflags='-N -l' # 仅将传递的编译选项应用于当前包
2go build -gcflags=all='-N -l' # 将传递的编译选项应用于当前包及其所有依赖包
3go build -gcflags=std='-N -l' # 仅将传递的编译选项应用于标准库包
go tool compile -help
- -l:关闭内联
- -N:关闭代码优化
- -m:输出代码优化决策,主要是逃逸分析 (决定哪些对象在栈上分配,哪些对象在堆上分配) 的过程
- -S:输出汇编代码
逃逸分析
1go build -gcflags='-m'
2go build -gcflags='-m -m' # 输出比上一条命令更为详尽的逃逸分析过程信息
3go build -gcflags='-m=2' # 与上一条命令等价
4go build -gcflags='-m -m -m' # 输出最为详尽的逃逸分析过程信息
5go build -gcflags='-m=3' # 与上一条命令等价
不指定参数,编译目录下所有文件
go build ./… 编译当前目录下,及子目录下所有 不产生任何结果
go install ./… 执行这个命令,把编译出来的命令放到bin下
go build -n 查看编译过程
packages
- 所编译的包名,如果不填写默认为编译当前路径下的入口文件,文件名称默认为当前文件夹名称
交叉编译
1CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
- Mac上编译Windows可执行二进制文件
1CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
- Linux上编译Mac可执行二进制文件
1CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
- Linux上编译Windows可执行二进制文件
1CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
- Windows上编译Mac可执行二进制文件
1SET CGO_ENABLED=0 SET GOOS=darwin SET GOARCH=amd64 go build main.go
- Windows上编译Linux可执行二进制文件
1SET CGO_ENABLED=0 SET GOOS=linux SET GOARCH=amd64 go build main.go
go install
compile and install packages and dependencies
用户编译并安装代码包或源码文件
安装代码包会在当前工作区的pkg/<平台相关目录>生成归档文件
安装命令源码文件会在当前工作区的bin目录或$GOBIN目录下生成可执行文件
go install [-i] [build flags] [packages] 和 build命令类似
go doc
- 获取go函数帮助文档
- 命令行形式获取某个包的介绍以及包下所有可用的公共方法列表
1go doc strconv
- 命令行形式获取某个方法的文档
1go doc strconv.Itoa
- 使用网页形式查看帮助文档
1godoc -http=localhost:6060
- 在浏览器中输入:http://localhost:6060/ 可以查看对应的文档信息
go env
系统内go相关的环境变量信息
go env 查询环境变量
go env -w GO111MODULES=ON
go env -w GOPROXY=https://goproxy.cn,direct
go test
- Go语言自带的测试工具,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件
- 原则
- 文件名必须是 _test.go 结尾的,这样在执行 go test 的时候才会执行到相应的代码
- 必须 import testing 这个包
go run
参数:
-
-v 列出被编译的代码包的名称
-
-work 显示编译时创建的工作目录的路径,并且不删除它
-
-x :打印编译过程中所需的运行的命令 并执行它们
-
-n : 打印编译过程中所需的命令,但并不执行
-
-p n 使用n个cpu去编译
-
-a 强制编译代码
go get
get download and install packages and dependencies
-
判断指定路径(这里的路径指go get /path/to)在本地是否存在: 如果存在,就读取import,然后下载依赖包 如果不存在,就从远程下载包,然后再读取下载的包里的import来判断是否迭代下载依赖包。
-
依赖包都下载完毕后判断go get是否有带-d参数,如果没有-d,则从迭代的最后一个依赖包开始往前一个个install,直到指定路径(这里的路径指go get /path/to)install完毕
用户获取 go的第三方包,通常会从go repo上pull最新的版本
常用命令如: go get -u github.com/go-sql-driver/mysql
从github上获取mysql的driver并安装至本地,内部使用git clone 命令
- -u 拉最新的, 强制使用网络去更新包和它的依赖包,下载并安装代码包,不论工作区中是否已存在它们。
- -v 显示执行的命令
- -x 看过程
- -d 下载代码包,不执行安装
-fix
:在下载代码包后先运行一个用于根据当前 Go 语言版本修正代码的工具,然后再安装代码包。-t
:同时下载测试所需的代码包。-insecure
:允许通过非安全的网络协议下载和安装代码包。HTTP 就是这样的协议。
指定版本拉取
go get -u go.uber.org/zap@1.11
go fmt
格式化代码
其他命令工具
godoc -http=localhost:6060
goimport
配置ide 的 File watchers为goimport