golang学习笔记

1.golang进阶

1.1 golang的异常处理

golang中deferpanicrecover是很常用的三个特性,三者一起使用可以充当其他语言中try…catch…的角色,而defer本身又像其他语言的析构函数。

1.1.1 defer

defer后边会接一个函数调用,函数调用,函数调用!!!,但该函数不会立刻被执行,而是等到包含它的程序返回时(包含它的函数执行了return语句、运行到函数结尾自动返回、对应的goroutine panic)defer函数才会被执行。通常用于资源释放、打印日志、异常捕获等

如果有多个defer函数,调用顺序类似于栈,越后面的defer函数越先被执行(后进先出)

1
2
3
4
5
6
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
}

output:

1
2
3
4
4
3
2
1

defer的深入理解:

return xxx并不是一条原子指令,含有defer函数的外层函数,返回的过程是这样的:

1
2
3
返回值 = xxx
调用defer函数(这里可能会有修改返回值的操作)
return 返回值

example 1:

1
2
3
4
5
6
func f() (result int) {
defer func() {
result++
}()
return 0
}

example 2:

1
2
3
4
5
6
7
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}

example 3:

1
2
3
4
5
6
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
defer函数的参数值,是在声明defer时确定下来的
  • 函数参数

  • 闭包引用

在defer函数声明时,对外部变量的引用有两种方式:作为函数参数和作为闭包引用
作为函数参数,在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算(如上边的example 3)而作为闭包引用,在defer函数执行时根据整个上下文确定当前的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
i := 0

// 函数参数
defer fmt.Println("a:", i)

//函数参数,将外部i传到闭包中进行计算,不会改变i的值,如上边的example 3
defer func(i int) {
fmt.Println("b:", i)
}(i)

//闭包引用,捕获同作用域下的i进行计算
defer func() {
fmt.Println("c:", i)
}()
i++
}

output:

1
2
3
c: 1
b: 0
a: 0

1.1.2 recover要与defer配对使用,并且不跨协程,才能真正拦截panic

  • panic 只会触发当前 Goroutine 的 defer

  • recover 只有在 defer 中调用才会生效;

  • panic 允许在 defer 中嵌套多次调用;

2.golang的依赖管理

GO语言的依赖管理经历了三个阶段:

  • GOPATH

  • vendor

  • go mod (终极解决方案)

GOPATH的弊端

  • 没有版本控制概念

  • 无法同步一致的第三方版本号

  • 无法指定当前项目的版本号

于是go mod 就诞生了

GOPATH目录下有binpkgsrc目录,go1.11之前需要将项目强制放在$GOPATH/src目录下,有了go module的概念后,项目可以放在任何位置。

2.1 go mod 命令管理模块依赖

go mod命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go mod init 生成go.mod文件

go mod download 下载go.mod中指明的所有依赖

go mod tidy 整理现有的依赖

go mod graph 查看现有的依赖结构

go mod edit 编辑go.mod文件

go mod vendor 导出项目所有的依赖到vendor目录

go mod verify 校验一个模块是否被篡改

go mod why 查看为什么需要依赖某模块

2.2 What is a module?

A module is a collection of related Go packages that are versioned together as a single unit.

  • 一个仓库包含若干个module

  • 一个module包含若干个package

  • 一个package包含若干个xxx.go文件

3.GO的常见环境变量

To show documentation for all go env variables and values you can run:

1
go help environment

GO111MODULE (GO1.11版本)

GO Modules是go语言的依赖解决方案,发布于Go1.11,成长于Go1.12,丰富于Go1.13,正式于Go1.14推荐在生产中使用。

Go Modules的出现解决了Go1.11版本之前几个常见的争议问题:

  • Go语言长期以来的依赖管理问题

  • “淘汰”现有的GOPATH模式

  • 统一社区中的其他依赖管理工具

Go语言提供了GO111MODULE这个环境变量作为Go modules的开关,允许设置如下值:

  • auto: 只要项目包含了go.mod文件就启动Go modules

  • on: 启用Go modules,推荐设置

  • off: 禁用Go modules,不推荐设置

1
go env -w GO111MODULE=on

Go Modules 的语义化版本控制

版本号递增规则如下:

  • 主版本号: 做了不兼容的API修改

  • 次版本号: 做了向下兼容的功能性新增

  • 修订号: 做了向下兼容的问题修正


GOPATH

是用户工作空间目录的环境变量,属于用户域的范畴,自Go1.11之后已经不推荐使用,

实质上它是个目录

1
2
$go env
GOPATH="/Users/morvan/go"

可以看出GOPATH变量的结果是一个路径,进入该路径继续查看,目录结构如下

1
2
3
4
5
6
7
go
|--bin
|--pkg
|--src
|--github.com
|--golang.org
...
  • bin: 存储编译生成的二进制文件

  • pkg: 存储预编译的目标文件,以加快程序后续的编译速度

  • src: 存储所有的.go文件或源代码

为什么弃用GOPATH?

GOPATH模式下没有版本控制的概念,通常会造成以下问题:

  1. 在执行go get时,无法传达任何的版本信息,因为它是以目录路径作为划分的。

  2. 在运行Go应用程序时,无法保证其他人与我们所期望依赖的第三方库的版本是同一个。

  3. 无法处理v1、v2、v3等不同版本的引用问题,因为在GOPATH模式下,导入路径是一样的,例如都是github.com/foo/bar

第三方版本库全局缓存在$GOPATH/pkg/mod$GOPATH/pkg/sumdb目录下,要清除全局缓存可以使用go clean -modcache命令

1
$go clean -modcacache

GOPROXY

从哪里拉取第三方包

其默认值为: https://proxy.golang.org,direct

1
go env -w GOPROXY=https://goproxy.cn,direct

GOPRIVATE

1
2
3
go env -w GOPRIVATE="git.example.com,github.com/vancholen/hello"

# 表示git.example.com和github.com/vancholen/hello是私有仓库,不会进行GOPROXY下载和校验

4.iota

  • itoa 是自动增长的

  • itoa 的这个增长是在一个"=" 作用域结束之后

  • iota是golang语言的常量计数器,只能在常量的表达式中使用。

    iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行(“行块”apple, banana = iota + 1, iota + 2 )索引)。

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
// itoa 是自动增长的
// itoa 的这个增长是在一个"=" 作用域结束之后

package main

import "fmt"

const (
login = iota // 0
logout // 1
user = iota + 1 // 3
account = iota + 3 // 6
)

// 这搞砸了,因为现在你的常量有相同的值。
const (
apple, banana = iota + 1, iota + 2 // 1 2
peach, pear // 2 3
orange, mango // 3 4
)

func main() {
fmt.Printf("%d %d %d %d\n", login, logout, user, account)
fmt.Printf("%d %d %d %d %d %d\n", apple, banana, peach, pear, orange, mango)
}

5.函数闭包实现类似数据库自增功能

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
// 函数闭包,子函数内部可以读取父函数作用域的变量

package main

import "fmt"

// getSeqence() 得到类似数据库中自增序列的功能
// 本来是临时地址的i在getSeqence返回后并不会被释放
func getSeqence() func() int {
i := 0
return func() int {
i += 1
return i
}
}

func main() {
nextnumber := getSeqence()
fmt.Println(nextnumber())
fmt.Println(nextnumber())
fmt.Println(nextnumber())

f := getSeqence()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}

6. 使用goroutine提升用户体验

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
// 使用goroutine提升用户体验

package main
import (
"fmt"
"time"
)
func spinner(delay time.Duration){
for{
for _, r := range `-\|/`{
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int{
if x<2 {
return x
}
return fib(x-2) + fib(x-1)
}
func main(){
go spinner(time.Millisecond * 100)
fmt.Printf("\n%d\n",fib(50))
}

7.关闭 channel的动作由写端发起——channel 实现数字传递游戏

关闭channel的动作一定要由写端发起!!! (谁负责写channel,谁就负责关闭channel)

关闭channel的动作一定要由写端发起!!!

关闭channel的动作一定要由写端发起!!!

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 3个goroutine 数字传递游戏
// 第1个 routine 负责生成0-9,传递给第2个routine
// 第2个 routine 接受到,然后平方 传递给第3个
// 第3个 routine 负责打印结果
// ! 关闭channel的动作一定要由写端发起 !
package main

import (
"fmt"
"time"
)

func first(c1 chan<- int) {
for i := 0; i < 10; i++ {
c1 <- i
time.Sleep(time.Second * 1)
}
close(c1) // 有写channel 1 ,则就要负责关闭channel 1
}

func second(c1 <-chan int, c2 chan<- int) {
for {
num, ok := <-c1
if ok {
c2 <- num * num
} else {
break
}
}
close(c2) // 有写channel 2 ,则就要负责关闭channel 2
}

var c1 chan int
var c2 chan int

func main() {
c1 = make(chan int)
c2 = make(chan int)
go first(c1)
go second(c1, c2)
for {
num, ok := <-c2
if ok {
fmt.Println(num)
} else {
break
}
}
}

8.“主线程”中等待所有goroutine执行完成

Golang 官方在 sync 包中提供了 WaitGroup 类型可以解决这个问题。其文档描述如下:

使用方法可以总结为下面几点:

  • 在父协程中创建一个 WaitGroup 实例,比如名称为:wg

  • 调用 wg.Add(n) ,其中 n 是等待的 goroutine 的数量

  • 在每个 goroutine 运行的函数中执行 defer wg.Done()

  • 调用 wg.Wait() 阻塞主逻辑

  • 直到所有 goroutine 执行完成。

使用sync.WaitGroup完善示例7:channel实现数字传递游戏 的代码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"time"
"sync"
)

var c1 chan int
var c2 chan int

var wg sync.WaitGroup

func produce(c1 chan<- int) {
for i := 0; i < 10; i++ {
c1 <- i
time.Sleep(time.Second)
}
close(c1)
defer wg.Done()
}

func deal(c1 <-chan int, c2 chan<- int) {
for {
num, ok := <-c1
if ok {
c2 <- num * num + 1
} else {
break
}
}
close(c2)
defer wg.Done()
}

func output(c <-chan int) {
for {
num, ok := <-c
if ok {
fmt.Println("num is ", num)
} else {
break
}
}
defer wg.Done()
}

func main() {
fmt.Println("Programm starting......")
c1 = make(chan int)
c2 = make(chan int)
wg.Add(1)
go produce(c1)
wg.Add(1)
go deal(c1, c2)
wg.Add(1)
go output(c2)

wg.Wait()
}

9.The Go Compiler

go tool compile main.go ==> main.o

go tool compile -pack main.go ==> main.a

go tool compile -S main.go ==> main.o和汇编代码

10. Calling C Code from Go

10.1 Calling C code from Go using the same file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

// #include<stdio.h>
// void callC(){
// printf("Calling C code!\n");
// }
import "C"

import "fmt"
func main(){
fmt.Println("A Go Statement!")
C.callC()
fmt.Println("Another Go Statement!")
}

10.2 Calling C code from Go using separate files

callC.h

1
2
3
4
5
6
7
#ifndef CALLC_H
#define CALLC_H

void cHello();
void printMessage(char* message);

#endif

callC.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "callC.h"

void cHello(){
printf("Hello from C!\n");
}

void printMessage(char* message){
printf("Go sent me %s\n", message);
}

编译命令

gcc -c *.c

ar rx callC.a *.o


main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

// #cgo CFLAGS: -I${SRCDIR}/callClib
// #cgo LDFLAGS: ${SRCDIR}/callC.a
// #include<stdlib.h>
// #include<callC.h>
import "C"


import (
"fmt"
"unsafe"
)
func main(){
fmt.Println("Going to call a C function!")
C.cHello()
fmt.Println("Going to call another C function!")
myMessage := C.CString("This is Me!")
defer C.free(unsafe.Pointer(myMessage))
C.printMessage(myMessage)

fmt.Println("All perfectly done!")
}

编译命令

go build -o main main.go

11.Calling Go functions from C code

userdByC.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "C"

import "fmt"

//export PrintMessage
func PrintMessage(){
fmt.Println("A Go function!")
}
//export Multiply
func Multiply(a,b int) int {
return a*b
}
func main(){
}

使用go build -o usedByC.o -buildmode=c-shared usedByC.go编译后会生成usedByC.husedByC.o两个文件


main.c文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#include "usedByC.h"

int main(int argc,char* argv[])
{
GoInt x = 12;
GoInt y = 90;
printf("About to call a Go function!\n");
PrintMessage();
GoInt p = Multiply(x,y);
printf("Product: %d\n",(int)p);
printf("It worked!\n");
return 0;
}

12. Type Assertion

A type assertion is the x.(T)notation where x is of interface type and T is a type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
func main(){
var myInt interface{} = 12;
k, ok := myInt.(int)
if ok{
fmt.Println("Success:",k)
}
v, ok := myInt.(float64)
if ok{
fmt.Println(v)
}else{
fmt.Println("Failed without panicking!")
}
}
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
35
36
37
38
39
package main 

import "fmt"

type square struct{
X float64
}

type circle struct{
R float64
}

type rectangle struct{
X float64
Y float64
}

func GetType(x interface{}){
switch v := x.(type){
case square:
fmt.Println("This is a square!")
case circle:
fmt.Printf("%v is a circle!\n",v)
case rectangle:
fmt.Println("This is a rectangle!")
default:
fmt.Printf("Unknown type %T\n",v)
}
}

func main(){
x := circle{R:10}
GetType(x)
y := rectangle{X:4,Y:10}
GetType(y)
z := square{X:9}
GetType(z)
GetType(10)
}

13. Go交叉编译

编译命令: env GOOS=windows GOARCH=amd64 go build xxx.go

环境变量GOOSGOARCH支持的值到如下网址查看:

https://go.dev/doc/install/source

14.Go 代码测试

testing标准包的使用

命令:go test xxx.go xxx_test.go -run='F1'

  • xxx.go中含有要被测试的函数实现,譬如有个函数f1xxx_test.go中的内容是测试代码,对应应该有Testf1(t *testing.T)

  • -run=F1表示只想测试那些函数名中有F1的那些函数

15.Benchmarking Go Code(基准测试)

Benchmarking can give you information about the performance of a function or a program in order to understand better how much faster or slower a function is compared to another function, or compared to the rest of the application.

A function signature like func (*tesing.B) will be invoked automatically by the go test command

16.解析config.json 配置文件

核心代码:json.NewDecoder(file).Decode(baseCfg)

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
package main

import (
"encoding/json"
"fmt"
"os"
)

type baseConfig struct {
Name string
Address string
Debug bool
}
var baseCfg = &baseConfig{}
// 解析config.json 配置文件
func parseBaseConfig(s string) (*baseConfig, error) {
file, err := os.Open(s)
if err != nil {
fmt.Println(err)
return nil, err
}
defer file.Close()

if err := json.NewDecoder(file).Decode(baseCfg); err != nil {
fmt.Println(err)
return nil, err
}
return baseCfg, nil
}
func main() {
parseBaseConfig("config.json")
}

config.json:

1
2
3
4
5
{
"Name": "Java",
"Debug": true,
"Address": "四川成都"
}

17.import导入的是包名?文件夹名?

答案:是路径文件夹名!!!

golang的import还支持如下两种方式来加载自己写的模块:

  • 相对路径import "./model" //当前文件同一目录的model目录,但是不建议这种方式import

  • 绝对路径import "shorturl/model" //加载GOPATH/src/shorturl/model模块

GO总是先从GOROOT这个环境变量先搜索,再从GOPATH列出的路径顺序中搜索,只要一搜索到合适的文件夹就理解停止。当搜索完了仍搜索不到包时,将报错。

包导入后,就可以使用这个包中的属性。使用包名.属性的方式即可。

  • import导入的参数是路径文件夹名,而非包名。

  • 尽管习惯将包名文件夹名保持一致,但这并不是强制要求。

  • .go源文件中引用包成员时,使用包名,而非路径名

  • 包名和和文件夹名有可能不一样,但为了更好的维护和更高的可读性,普遍的做法是包名和目录名一致,如若不一致,import的时候要写文件夹名,引用的时候要写包名

  • 同一文件夹下,所有.go文件必须使用相同的包名称(因为导入时使用绝对路径,所以在搜索路径下,包必须有唯一路径,但无须是唯一名字);

当发现VS Code提示golang中的某些包找不到(具体表现为有红色下划波浪线),请使用VS Code打开xxx.mod所在的文件夹

18.Viewing Go Documentation

You can view documentation for the standard library packages via your terminal using the go doc tool. I often use this during development to quickly check something — like the name or signature of a specific function. I find it faster than navigating the web-based documentation and it’s always available offline too.

1
2
3
4
5
$ go doc strings            # View simplified documentation for the strings package
$ go doc -all strings # View full documentation for the strings package
$ go doc strings.Replace # View documentation for the strings.Replace function
$ go doc sql.DB # View documentation for the database/sql.DB type
$ go doc sql.DB.Query # View documentation for the database/sql.DB.Query method

You can also include the -src flag to display the relevant Go source code. For example:

1
$ go doc -src strings.Replace   # View the source code for the strings.Replace function

an-overview-of-go-tooling

19.Channel Communication

  • A send on a channel is synchronized before the completion of the corresponding receive from that channel.

  • The closing of a channel is synchronized before a receive that returns a zero value because the channel is closed.

  • A receive from an unbuffered channel is synchronized before the completion of the corresponding send on that channel.

  • The kth receive on a channel with capacity C is synchronized before the completion of the k+Cth send from that channel completes.

题目1😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

var a string
var c = make(chan int)

func f() {
a = "hello,gopher!"
c <- 0
}

func main() {
go f()
<-c
fmt.Println(a)
}

输出 hello,gopher!

题目2😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

var a string
var c = make(chan int)

func f() {
a = "hello,gopher!"
<-c
}

func main() {
go f()
c <- 0
fmt.Println(a)
}

输出 hello,gopher!

题目3😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

var a string
var c = make(chan int, 1)

func f() {
a = "hello,gopher!"
<-c
}

func main() {
go f()
c <- 0
fmt.Println(a)
}

// It might print the empty string, crash, or do something else.

题目4😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

var a string
var c = make(chan int, 1)

func f() {
a = "hello,gopher!"
c <- 0
}

func main() {
go f()
<-c
fmt.Println(a)
}

输出hello,gopher!

题目5😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

var a string
var c = make(chan int)

func f() {
a = "hello,gopher!"
close(c)
}

func main() {
go f()
<-c
fmt.Println(a)
}

输出hello,gopher!

文章作者: 小王同学
文章链接: https://morvan.top/2021/11/15/golang学习笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 小王同学的精神驿站