Go语言学习

本文最后更新于:2023年6月18日 下午

Go语言学习

本笔记基于菜鸟教程Go语言教程和Go语言编程编写,详情请点击Go语言教程Go语言之旅

第一个Go语言教程

hello.go文件

1
2
3
4
5
6
package main
import "fmt"
func main(){
/*我的第一个Go语言程序*/
fmt.Println("Hello,World!")
}

运行go语言代码。

1
2
$ go run hello.go
Hello,World!

编译生成二进制文件

1
$ go build hello.go

Go语言的基础组成部分有以下几个部分;

  • 包声明

    hello.go文件中package main定义了包名,你必须在源文件中非注释的第一行指明这个文件属于哪个包,package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

  • 引入包

    import "fmt"表明导入了fmt包,跟python一致,fmt 包实现了格式化 IO(输入/输出)的函数。

    通过 import 关键字来导入其他非 main 包。

    可以通过 import 关键字单个导入:

    1
    2
    import "fmt"
    import "io"

    也可以同时导入多个:

    1
    2
    3
    4
    import (
    "fmt"
    "math"
    )

    省略调用(不建议使用):

    1
    2
    // 调用的时候只需要Println(),而不需要fmt.Println()
    import . "fmt"
  • 函数

    func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

  • 变量&语句&表达式

    当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

  • 注释

    单行注释//,多行注释/*开头,*/结尾,与c语言一致。

Go语言基础语法

行标识符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

字符串拼接

Go语言字符串拼接使用+实现:

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("Hello" + "," + "World")
}

可见性规则

Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。

函数名首字母小写即为 private :

1
func getId() {}

函数名首字母大写即为 public :

1
func Printf() {}

Go fmt包(输入输出)

输出相关函数

Print() 函数将参数列表 a 中的各个参数转换为字符串并写入到标准输出中。

非字符串参数之间会添加空格,返回写入的字节数。

1
func Print(a ...interface{}) (n int, err error)

Println() 函数功能类似 Print,只不过最后会添加一个换行符。

所有参数之间会添加空格,返回写入的字节数。

1
func Println(a ...interface{}) (n int, err error)

Printf() 函数将参数列表 a 填写到格式字符串 format 的占位符中。

填写后的结果写入到标准输出中,返回写入的字节数。

1
func Printf(format string, a ...interface{}) (n int, err error)

以下三种与上面三种类似,不过最终会返回输出的字符串,并不会打印

1
2
3
func Sprint(a ...interface{}) string
func Sprintln(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string

以下函数功能同 Sprintf() 函数,只不过结果字符串被包装成了 error 类型。

1
func Errorf(format string, a ...interface{}) error

示例

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
func main() {
fmt.Print("a", "b", 1, 2, 3, "c", "d", "\n")
fmt.Println("a", "b", 1, 2, 3, "c", "d")
fmt.Printf("ab %d %d %d cd\n", 1, 2, 3)
// ab1 2 3cd
// a b 1 2 3 c d
// ab 1 2 3 cd

if err := percent(30, 70, 90, 160); err != nil {
fmt.Println(err)
}
// 30%
// 70%
// 90%
// 数值 160 超出范围(100)
}

func percent(i ...int) error {
for _, n := range i {
if n > 100 {
return fmt.Errorf("数值 %d 超出范围(100)", n)
}
fmt.Print(n, "%\n")
}
return nil
}

输入相关函数

Scan()从标准输入中读取数据,并将数据用空白分割并解析后存入 a 提供的变量中(换行符会被当作空白处理),变量必须以指针传入。

当读到 EOF 或所有变量都填写完毕则停止扫描。

返回成功解析的参数数量。

1
func Scan(a ...interface{}) (n int, err error)

ScanlnScan 类似,只不过遇到换行符就停止扫描。

1
func Scanln(a ...interface{}) (n int, err error)

Scanf 从标准输入中读取数据,并根据格式字符串 format 对数据进行解析,将解析结果存入参数 a 所提供的变量中,变量必须以指针传入。

输入端的换行符必须和 format 中的换行符相对应(如果格式字符串中有换行符,则输入端必须输入相应的换行符)。

占位符 %c 总是匹配下一个字符,包括空白,比如空格符、制表符、换行符。

返回成功解析的参数数量。

1
func Scanf(format string, a ...interface{}) (n int, err error)

以下三个函数功能同上面三个函数,只不过从 r 中读取数据。

1
2
3
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

以下三个函数功能同上面三个函数,只不过从 str 中读取数据。

1
2
3
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

示例

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
// 对于 Scan 而言,回车视为空白
func main() {
a, b, c := "", 0, false
fmt.Scan(&a, &b, &c)
fmt.Println(a, b, c)
// 在终端执行后,输入 abc 1 回车 true 回车
// 结果 abc 1 true
}

// 对于 Scanln 而言,回车结束扫描
func main() {
a, b, c := "", 0, false
fmt.Scanln(&a, &b, &c)
fmt.Println(a, b, c)
// 在终端执行后,输入 abc 1 true 回车
// 结果 abc 1 true
}

// 格式字符串可以指定宽度
func main() {
a, b, c := "", 0, false
fmt.Scanf("%4s%d%t", &a, &b, &c)
fmt.Println(a, b, c)
// 在终端执行后,输入 1234567true 回车
// 结果 1234 567 true
}

Go语言数据结构

布尔型

1
2
bool
布尔型变量只能为常量truefalse,默认为false

示例

1
2
3
4
5
6
7
8
//示例代码
var isActive bool // 全局变量声明
var enabled, disabled = true, false // 忽略类型的声明
func test() {
var available bool // 一般声明
valid := false // 简短声明
available = true // 赋值操作
}

数字类型

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
uint8
无符号8位整型
uint16
无符号16位整型
uint32
无符号8位整型
uint64
无符号64位整型

int8
有符号8位整型
int16
有符号16位整型
int32
有符号32位整型
int64
有符号64位整型

float32
IEEE-754 32位浮点型数
float64
IEEE-754 64位浮点型数
complex64
32 位实数和虚数
complex128
64 位实数和虚数

byte
字节流,类似uint8
rune
类似于int32,表示一个Unicode码点
uint
无符号3264
int
有符号3264
uintpte
无符号整型,用于存放指针

从Go1.9版本开始,对于数字类型,无需定义int及float32、float64,系统会自动识别。

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"

func main() {
var a = 1.5
var b =2
fmt.Println(a,b)
}
//运行结果
//1.5 2

派生类型

1
2
3
4
5
6
7
8
(a) 指针类型(Pointer)
(b) 数组类型
(c) 结构化类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型

Go安装依赖包

安装完成Golang之后,我们可以使用go env,查看基本配置信息

1
2
3
GOPATH="/*/*/*"
GOROOT="/*/*/*"
GOBIN=""

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
2
3
4
5
6
# Windows
set GO111MODULE=on
# Linux
export GO111MODULE=on
# mac
export GO111MODULE=on

Go module基本命令

初始化

在创建一个新的Golang项目时,我们可能没有创建go.mod文件,因此我们可以使用下面命令初始化一个module,模块名为你的项目名。

1
go mod init <模块名>

可以看到已经初始化了go.mod,只是内部没有任何依赖信息。

image-20230322160033269

更新依赖

1
go mod tidy

tidy会检测该文件夹目录下所有引入的依赖,并写入go.mod,不使用或错误的依赖也会自动进行删除。

下载依赖

1
go mod download

目前所有模块版本数据均缓存在$GOPATH/pkg/mod$GOPATH/pkg/sum下,同时会在项目根目录下生成 go.sum 文件, 该文件是依赖的详细依赖。

导入依赖

1
go mod vendor

我们的依赖下载完成后是存储了$GOPATH目录下的,如果有特殊需要,需要将GOPATH下的依赖转移至该项目根目录下的 vendor(自动新建) 文件夹下,可以使用该命令。

校验依赖

1
go mod verify

参考链接

go安装依赖包(go get, go module)

Go语言基础知识

https://go-zh.org/

Go语言之旅

每个 Go 程序都是由包构成的。

程序默认从main包开始运行。

按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"math/rand"
)

func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}

导入

Go语言与python类似,都是能够导入其他程序包,导入方式一般有两种,分组导入或多个导入语句

1
2
3
4
5
6
7
8
9
// 导入多条语句
import "fmt"
import "math"

// 分组导入
import (
"fmt"
"math"
)

导出名

在Go中,以大写字母开头,那么它就是已导出的已导出表示能够在包外访问,Go语言中调用其他包的函数都是已导出的函数。

函数

函数类型声明

go语言的函数声明与C语言函数声明类似,函数可以没有参数或接受多个参数,但是与c语言不同的是,函数参数类型说明位置不同,关于Go语言为什么采用这种声明方式,可以参考这篇文章,总而言之Go 的声明从左到右阅读,C 的声明是螺旋式的。

1
2
3
4
5
6
7
// c语言函数声明
int add(int x,int y)
{...}

// go语言函数声明
func add(x int,y int) int
{...}

go语言中函数类型是可以省略的,当连续两个或多个函数的已命名形参类型相同时,除最后一个类型外,其他都可以省略。上述例子简写为:

1
2
func add(x,y int) int
{}

多值返回

go语言中的函数能够返回任意数量的返回值,这是c语言所不具有的。

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

命名返回值

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。没有参数的 return 语句返回已命名的返回值。也就是 直接 返回。

1
2
3
4
5
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}

变量

var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。

1
var c, python, java bool

同时变量也可以进行初始化操作,每个变量对应一个。如果初始化值存在,可以省略类型;变量会从初始值中获取类型。

1
2
var i, j int = 1, 2
var c, python, java = true, false, "no!"

短变量

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

零值

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)

类型转换

Go语言类型转换是显式类型转换,表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

1
2
3
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

常量

常量的声明与变量类似,只不过是使用 const 关键字。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

1
const Pi = 3.14

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。

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

1
2
3
4
5
6
7
const (
a = iota // 0
b = iota // 1
c = iota // 2
d = "12" // 独立值,iota +=1
e = iota // 4
)

循环结构

Go语言中只有一种循环结构:for循环。对你没看错,只有这一种,while循环都是通过for变形而来。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式:在每次迭代前求值
  • 后置语句:在每次迭代的结尾执行

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

1
2
3
for i := 0; i < 10; i++ {
sum += i
}

Go中的while循环

1
2
3
4
5
6
for sum < 1000 {
sum += sum
}
// while(true) 无限循环
for {
}

条件控制语句

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( )而大括号 { } 则是必须的.

1
2
3
if x < 0 {
return sqrt(-x) + "i"
}

if语句同样可以在条件表达式前执行一个简单的语句,比如声明变量,该变量的作用域仅在ifelse之内。

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
2
3
4
5
6
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
// output: 3210
  • 如果defer执行的函数存在参数,那么该参数就是执行defer时值,并不会因为后续的操作而变化。例如:
1
2
3
4
5
6
7
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
// output: 0
  • defer可以修改调用函数的命名返回值。
1
2
3
4
5
6
func c() (i int) {
i = 3
defer func() { i++ }()
return 1
}
// c() return 2
  • return首先会对返回值进行值拷贝,将return语句返回的值复制到函数返回值栈区(如果只有return,不带任何变 或值,则此步骤不做任何动作)。

    1
    2
    3
    4
    5
    6
    func d() (i int) {
    t := 3
    defer func() { t++ }()
    return t
    }
    // c() return 3

实际上,defer语句是本函数return语句进行调用的,举个例子:

1
2
3
4
5
func f() (result int)
defer fun(){
}
return
}

实际上执行流程为

1
2
3
返回值(有名返回值存储在栈上,实际上修改栈上返回值数据) = xxx
调用defer函数(如果defer能够获栈上的返回值,则可以修改)
return

defer函数的参数值,是在申明defer时确定下来的。在defer函数申明时,对外部变量的引用是有两种方式:作为函数参数和作为闭包引用

  • 作为函数参数,在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算
  • 而作为闭包引用,在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的值,如上边的例3
defer func(i int) {
fmt.Println("b:", i)
}(i)
//闭包调用,捕获同作用域下的i进行计算
defer func() {
fmt.Println("c:", i)
}()
i++
}
// output
// c: 1
// b: 0
// a: 0

panic

​ 内置函数,能够直接终止程序控制流,同时调用本函数defer,并将panic向上层传递,直到整个程序返回异常。

Recover

​ 终止panic的函数,一般写在defer中,能够终止panic的终止信号,并停止向上层传递,此时程序将正常返回。

1
2
3
4
5
6
7
8
9
func F() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常:", err)
}
fmt.Println("b")
}()
panic("a")
}

recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的

数据类型

指针

Go语言中存在指针,保存值的内存地址,指针的操作与C语言中的类似,但是,Go中默认没有指针运算,即不支持++/--操作。

1
2
3
var p *int
p = &i
*p = 21

当然,如果你真的想用指针运算的话,可以使用uintptr 实现,uintptr支持指针运算,只不过普通指针需要需要先转换为unsafe.Pointer,然后在转换为uintptr (没有uintptr 与普通指针的转换方式)。

1
2
3
4
5
a:=[3]int8{6,8,9}
a_first_point:=&a[0]
a_first_unsafe_point:=unsafe.Pointer(a_first_point)
a_uintptr_first_unsafe_point:=uintptr(a_first_unsafe_point)
a_uintptr_first_unsafe_point++

结构体

Go语言结构体声明和使用与C语言类似,使用struc_name.filed_name访问。

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

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
v.X = 4
p := &v
fmt.Println(v.X)
fmt.Println((*p).X)
fmt.Println(p.X)
}

值得注意的是,如果使用结构体指针,Go语言中允许我们使用隐式间接引用struc_ptr.filed_name,而并非C语言的struc_ptr->filed_name形式。

Go语言结构题可以通过直接列出字段的值来新分配一个结果。

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

切片与数组

每个数组的大小都是固定的,而切片可以动态提取数组中的一部分,切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

1
a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。切片拥有长度容量,切片的长度它所包含的元素个数,使用len(s)获取;切片的容量是从它的第一个元素(注意是切片选取的,并不是数组的)开始数,到其底层数组元素末尾的个数,使用cap(s)获取。

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

1
a[:5]

切片的结构如下图所示,

image-20230415134148507

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。但是,如果一个引用数组的切片进行了copy或者append等操作时,这个切片就与原先的数组脱离的联系,仅仅内容一致而已。

创建数组时我们需要指定数组的长度,而切片可以从已创建数组中截取,也可以新建一个数组然后引用:

1
2
3
4
5
6
// 创建数组
q := []int{2, 3, 5, 7, 11, 13}
// 创建切片数组,此处会创建一个和上面相同的数组,然后构建一个引用了他的切片
q := []int{2, 3, 5, 7, 11, 13}
// 使用动态方法创建切片
q := make([]int, 0, 5) // len(q) = 0 , cap(q) = 5

函数闭包

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。Go通常使用这种方式来保存函数内部的局部变量,这与C语言中的静态局部变量功能类似,但有所区别:

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

import "fmt"

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
// output
// 0 0
// 1 -2
// 3 -6
// 6 -12
// 10 -20
// 15 -30
// 21 -42
// 28 -56
// 36 -72
// 45 -90

在上述程序中posneg所形成的闭包是不同的,因此其内部的局部变量也是不同的,C语言中无论如何调用函数,静态局部变量始终是同一个。

斐波那契闭包

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

import "fmt"

// 返回一个“返回int的函数”
func fibonacci() func() int {
i := 0
j := 1
return func() int{
r := i
i = j
j = r + j
return r
}
}

func main() {
f := fibonacci()
for i := 0; i < 15; i++ {
fmt.Println(f())
}
}

方法

​ Go中不存在类的概念,但是可以为结构体定义方法。方法就是一类带特殊接收者参数的函数(可以理解为数据类型的类函数)。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

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

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
// 或
fmt.Println(Abs(v))
}

注意,方法的声明只能为同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。

如果需要声明其他包类型,可以在本包内使用type重新声明一个类型别名

​ 当然由于Go语言函数值是值传递,因此,如果我们需要在方法内修改接收者的内容,则传递传递指针,即为指针接收者声明方法。另外,Go语言为了方便起见,变量和指向变量的指针是隐式转换的,我们都可以通过v.fun()直接访问该类型的方法。当然如果是普通函数传参的话,指针和变量无法隐式转换,是会报编译错误,望周知。

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

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())

p := &Vertex{4, 3}
p.Scale(3)
fmt.Println(p.Abs())
}
// output: 50 15

接口

​ 接口类型是有一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值。如果某个类型实现了接口中的所有方法,我们就说该类型实现了该接口。个人理解就是接口是Go语言中类似于多态的一种机制,接口类型就是虚基类。

与方法不同的是,如果我们使用接口类型多态执行相关函数,需要严格判断参数的类型,此时指针类型与变量类型无法被认为是同一种。

接口也是值,它们可以像其他值一样传递。接口的内部类型可以简单理解为valuetype的结构体(实际上接口类型更加复杂,但是在作用上类似于类型和值),接口的实现就是通过type找到相应数据的底层类型,调用其底层类型的同名方法。

接口类型断言

类型断言提供了访问接口值底层具体值的方式。

1
t := i.(T)

该语句断言(确信)接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。若 i 并未保存 T 类型的值,该语句就会触发一个panic

为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。

1
t, ok := i.(T)

i 保存了一个 T,那么 t 将会是其底层值,而 oktrue;否则,ok 将为 falset 将为 T 类型的零值,程序并不会产生恐慌。

接口类型查询

​ 接口类型查询用于查询一个接口变量底层的具体类型是什么,或者该接口变量是否实现了其他接口。类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}

此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 TS 的情况下,变量 v 会分别按 TS 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 vi 的接口类型和值相同。

Stringer接口

fmt 包中定义的 Stringer 是最普遍的接口之一。

1
2
3
type Stringer interface {
String() string
}

Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值(感觉像是php中的_toString()_方法(っ °Д °;)っ)。

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

import "fmt"

type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}

error接口

Go 程序使用 error 值来表示错误状态。

1
2
3
type error interface {
Error() string
}

通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil 来进行错误处理。

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

import (
"fmt"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string{
return fmt.Sprint("cannot Sqrt negative number: %v",float64(e))
}

func Sqrt(x float64) (float64, error) {
if x<=0{
return 0, ErrNegativeSqrt(x)
}
return 0, nil
}

func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}

go语言并发

信道

信道是带有类型的管道,使用<-来发送和接受数据。

Channel类型定义

1
2
3
chan T          // 可以接收和发送类型为 T 的数据
chan<- int //只可以用来发送int数据
<-chan int //只可以用来接受int数据
1
2
3
ch := make(chan int)  // 创建int类型信道ch,默认是双向信道
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

信道也可以设置缓冲区,在这种情况下,仅当缓冲区填满后,发送方才会阻塞;仅当缓冲区为空时,接收方才会阻塞。

1
ch := make(chan in,2)

信道默认情况下不需要显示的进行关闭但是在某些情况下可能需要表示数据传输完成,可以使用close(ch)关闭指定信道,此时,下列ok属性会被职位false,代表信道关闭,不能继续传入数据。循环 for i := range ch 会不断从信道接收值,直到它被关闭。

1
v, ok := <-ch

select语句

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

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

import "fmt"

func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

默认情况下,主函数读取信道c,case c<-x分支即可继续,case <-quit一直阻塞;当执行到quit <- 0,信道c阻塞,信道quit存入数据,case c<-x分支阻塞,case <-quit执行继续。

select中也存在默认分支,当其他分支都没有准备好时,default分支就会执行。

练习

判断等价二叉查找树

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

import (
"fmt"

"golang.org/x/tour/tree"
)

// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
if t == nil {
return
}
Walk(t.Left, ch)
Walk(t.Right, ch)
ch <- t.Value
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
ch1 := make(chan int)
ch2 := make(chan int)
go Walk(t1, ch1)
go Walk(t2, ch2)
for i := 0; i < 10; i++ {
if <-ch1 != <-ch2 {
return false
}
}
return true

}

func main() {
ch := make(chan int)
go Walk(tree.New(1), ch)
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
t1:=tree.New(1)
t2:=tree.New(2)
fmt.Println(Same(t1, t1))
fmt.Println(Same(t1, t2))
}

web并发爬虫

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package main

import (
"fmt"
"sync"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

type SaveUrl struct {
url map[string] int
mux sync.Mutex
}

var saveurls = SaveUrl{url: make(map[string] int)}

func (s * SaveUrl)insert(url string) bool{
s.mux.Lock()
defer s.mux.Unlock()
_,ok := s.url[url]
if ok{
return false
}
s.url[url]++
return true
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher,end chan bool) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
end <- true
return
}
if saveurls.insert(url) == false{
end <- true
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
end <- true
return
}

fmt.Printf("found: %s %q\n", url, body)

ch := make(chan bool)
for _, u := range urls {
go Crawl(u, depth-1, fetcher,ch)
}
for i :=0;i<len(urls);i++{
<-ch
}
end <- true
return
}

func main() {
end := make(chan bool)
go Crawl("https://golang.org/", 4, fetcher, end)
<-end
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
body string
urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

Go并发

GOMAXPROCS

GOMAXPROCS 是 Golang 提供的非常重要的一个环境变量设定。通过设定 GOMAXPROCS,用户可以调整 Runtime Scheduler 中 Processor(简称P)的数量。Processor 是类似于 CPU 核心的概念,其用来控制并发的 M 数量。

1
2
3
4
// 查看当前GOMAXPROCS数量
fmt.Println(runtime.GOMAXPROCS(0))
// 设置GOMAXPROCS数量为CPU核心数
runtime.GOMAXPROCS(runtime.NumCPU())

func Goexit

func Goexit用于结束当前goroutime的运行, Go exit 在结束当前 goroutine 运行之前会调用当前 goroutine 已经注册的 deferGoexit 并不会产生 panic ,所以该 goroutine defer 里面的 recover 调用都返回 ni

func Gosched

func Gosched是放弃当前调度执行机会,将当前 goroutine 放到队列中等待下次被调度。


Go语言学习
https://genioco.github.io/2022/07/12/Learn/Go语言学习笔记/
作者
BadWolf
发布于
2022年7月12日
许可协议