golang知识图谱
基础知识
go 语言关键字、标识符、数据类型、变量、流程控制、函数、数组、闭包
关键字
switch
构造的一种形式。我们在切换后指定一个变量。chan
关键字用于定义通道。在执行
中,允许您同时运行并行代码。const
关键字用于标量值引入名称,常量continue
使用关键字可以返回到for
循环的开头,跳过当前循环default
语句是可选的,在switch
语句中使用case和default.如果值与表达式不匹配,则跳到默认值。defer
用于推迟执行功能,直到周围的功能执行为止,如果是在函数中最后执行if
条件为假,则执行else下的语句switch
语句中使用该关键字。当我们使用该关键字时,将执行下面的case条件for
开始for循环func
关键字声明一个函数go
关键字触发一个goroutine(异步处理),该例程由golang运行时管理goto
关键字可无条件跳转至带标签的语句if
语句用于检查循环内的特定条件。import
关键字用于导入软件包。interface
关键字用于指定方法集。方法集时一种类型的方法列表。map
关键字定义map类型。映射是键值对的无序集合。package
关键字代码在包中分组为一个单元。类似代码文件在文件夹中的统一包名。range
关键字可以迭代列表(map或者数组)。遍历循环(map或者数组)。select
关键字使goroutine在同步通信操作期间等待处理。struct
是字段的集合。我们可以在字段声明后使用struct关键字,定义结构体。switch
语句用于启动循环并在块内使用if-else逻辑。type
我们可以使用type
关键字引入新的结构类型。var
关键字用于创建go语言的变量标识符
标识符是指go语言对各种函数、方法、变量等命名时使用的字符序列,标识符由若干个字母、下划线
_
、和数字组成,并且第一个字符必须是字母。 下划线_
是一个特殊的标识符,称之为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),它赋值给它的值都将被抛弃,因此不可以使用_
作为变量给其他变量进行赋值或运算。 变量、类型、函数或者代码内标识符的名称不能重复。
_
组成break
、if
等数据类型
bool - 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 数字类型 - 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 字符串类型 - string 错误类型 派生类型: - 指针类型 pointer - 数组类型 - 结构化类型 struct - 通道类型 channel - 函数类型 - 切片类型 - 接口类型 interface - map类型
指针类型
类型指针:允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
切片指针:由指向起始元素的原始指针、元素数量和容量组成。 (变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址
)
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。
数组类型
数组被称为array,就是一个由若干相同类型的元素组成的序列。 注意:数组的长度是数组类型的一部分。只要类型声明中数组长度不同,即使两个数组类型的元素类型相同,它们还是不同的类型。列入:[2]string和[3]string 数组类型的下标都是整数
结构化类型 struct
基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物所有或者部分的属性时,go语言提供了一种自定义的数据类型,可以封装多个基础数据类型,这种数据类型叫做结构体。struct
通道类型 channel
通道
channel
是一种特殊的类型 在任何时候,同时只能有一个goroutine
访问通道进行发送和获取数据。goroutine之间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先进先出
的规则,保证收发数据的顺序。
特点:
把数据往通道中发送时,如果接收方一直没有接收,那么发送操作将持续阻塞。
如果接收方接收时,通道中没有发送方发送的数据,接收方也会发送阻塞,直到发送方发送数据为止
通道一次只能接收一个元素。
函数类型
可以把函数作为一种变量,用
type
去定义它,那么这个函数类型就可以作为值传递 type calsulTest func(int,int) //声明一个函数类型
切片类型
数据结构是切片,动态数组,其长度并不固定,可以在切片中追加元素,它会在容量不足时自动扩容。 切片长度可以随着元素数量的增长而增长(
但不会随着元素的数量减少而减少
) 切片数据类型是有如下结构体表示的 - Data 是指向数组的指针 - Len 是当前切片的长度 - Cap 是当前切片的容量大小,即Data数组的大小 切片占用的内存空间=切片中元素大小 X 切片容量 切片自动扩容,扩容后新切片的容量将会是原切片容量的2倍,如果还是不足以容纳新元素,则按照同样的操作继续扩容,直到新容量不小于原长度与追加的元素数量之和。 切片扩容是生成容量更大的切片,把原有元素和新元素一并copy到新切片中。
接口类型 interface
interface是一种类型,从它的定义可以看出用了
type
关键字,准确的来说interface是一种具有一组方法的类型
. interface被多种类型实现时,需要区分interface的变量时那种储存类型的值,go需要用断言方式。go 可以使用 comma, ok 的形式做区分 value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。
map类型
map是一堆键值对的未排序集合,类似Python中字典的概念,它的格式为map[keyType]valueType,是一个key-value的hash结构。map的读取和设置也类似slice一样,通过key来操作,只是slice的index只能是int类型,而map多了很多类型,可以是int,可以是string及所有完全定义了==与!=操作的类型。
var map变量名 map[key] value
关键字: map make delete
变量
var 声明语句可以创建一个特定类型的变量,然后给变量附加名称,并且设置初始值
var 变量名称 类型 = 表达式 var aa string = "golang"
简洁声明
:
aa := "golang"
初始化一组变量
i,j := 0,1
:=
是一个变量声明语句=
是一个变量赋值操作指针
一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或者x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。
一个指针的值是另外一个变量的地址。一个指针对应变量在内存中的储存位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。
如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时p表达式对应p指针指向的变量的值。一般p表达式读取指针指向的变量的值,这里为int类型的值,同时因为p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
p := &x // p, of type *int, points to x fmt.Println(*p) // "1" *p = 2 // equivalent to x = 2 fmt.Println(x) // "2"
任何类型的指针的零值都是nil.如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
var x, y int fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
流程控制
if else (分支结构)
关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。
if condition { // do something } else { // do something }
for (循环结构)
与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和 C++ 中非常接近:
sum := 0 for i := 0; i < 10; i++ { sum += i }
for 中的结束语句——每次循环结束时执行的语句
在结束每次循环前执行的语句,如果循环被 break、goto、return、panic 等语句强制退出,结束语句不会被执行。
for range (键值循环)
for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句,一般形式为:
for key, val := range coll { fmt.Println(key,val) }
通过 for range 遍历的返回值有一定的规律:
switch case 语句
switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:
var a = "hello" switch a { case "hello": fmt.Println(1) // 输出 1 case "world": fmt.Println(2) default: fmt.Println(0) }
一分支多值 (多个条件对应一个值)
var a = "mum" switch a { case "mum", "daddy": fmt.Println("family") }
分支表达式
var r int = 11 switch { case r > 10 && r < 20: fmt.Println(r) }
跨越 case 的 fallthrough——兼容C语言的 case 设计
在Go语言中 case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了 fallthrough 关键字来实现这一功能
var s = "hello" switch { case s == "hello": fmt.Println("hello") fallthrough case s != "world": fmt.Println("world") } // 输出 hello world
goto语句——跳转到指定的标签
goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。
package main import "fmt" func main() { for x := 0; x < 10; x++ { for y := 0; y < 10; y++ { if y == 2 { // 跳转到标签 goto breakHere } } } // 手动返回, 避免执行进入标签 return // 标签 breakHere: fmt.Println("done") } // 标签只能被 goto 使用,但不影响代码执行流程,此处如果不手动返回,在不满足条件时,也会执行第 24 行代码。 // 输出 y=2时候跳到标签breakHere,输出done // 使用场景:打印日志等
函数
函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句,每一个程序都包含很多的函数,函数是基本的代码块。 当函数执行到代码块最后一行}之前或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数,这些参数将作为返回值供调用者使用,简单的 return 语句也可以用来结束 for 的死循环,或者结束一个协程(goroutine)。
三种类型的函数:
普通函数声明(定义)
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func aa(a int) int { return a } fmt.Println(aa(123)) // 123 // func 函数名(形式参数列表)(返回值列表){ 函数体 }
函数变量——把函数作为值保存到变量中
函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数
package main import ( "fmt" ) func fire() { fmt.Println("fire") } func main() { var f func() f = fire f() } // 输出 fire
package main import ( "fmt" ) func fire() int { return 24 } func main() { f := func() int { return 0 } f = fire fmt.Println(f()) } // 输出 24 // 函数变量 f 进行函数调用,实际调用的是 fire() 函数。
匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。
f := func(aa int) { fmt.Println(aa) return } // 使用f()调用 f(24) // 输出 24
匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
package main import ( "fmt" ) // 遍历切片的每个元素, 通过给定函数进行元素访问 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 使用匿名函数打印切片内容 visit([]int{1, 2, 3, 4}, func(v int) { fmt.Println(v) }) } // 输出 1 2 3 4 // 使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
defer(延迟执行语句)
defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。 逆序执行(类似栈,即后进先出)
package main import ( "fmt" ) func main() { fmt.Println("defer begin") // 将defer放入延迟调用栈 defer fmt.Println(1) defer fmt.Println(2) // 最后一个放入, 位于栈顶, 最先调用 defer fmt.Println(3) fmt.Println("defer end") } // 输出 // defer begin // defer end // 3 // 2 // 1
使用延迟执行语句在函数退出时释放资源
处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。 defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。
使用延迟并发解锁
函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁
var ( // 一个演示用的映射 valueByKey = make(map[string]int) // 保证使用映射时的并发安全的互斥锁 valueByKeyGuard sync.Mutex ) // 根据键读取值 func readValue(key string) int { // 对共享资源加锁 valueByKeyGuard.Lock() // 取值 v := valueByKey[key] // 对共享资源解锁 valueByKeyGuard.Unlock() // 返回值 return v } // 实例化一个 map,键是 string 类型,值为 int。 // map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。 // readValue() 函数给定一个键,从 map 中获得值后返回,该函数会在并发环境中使用,需要保证并发安全。 // 使用互斥量加锁。 // 从 map 中获取值。 // 使用互斥量解锁。 // 返回获取到的 map 值。
使用 defer 语句对上面的语句进行简化
func readValue(key string) int { valueByKeyGuard.Lock() // defer后面的语句不会马上调用, 而是延迟到函数结束时调用 defer valueByKeyGuard.Unlock() return valueByKey[key] }
使用延迟释放文件句柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源
func fileSize(filename string) int64 { f, err := os.Open(filename) if err != nil { return 0 } // 延迟调用Close, 此时Close不会被调用 // 注意,不能将这一句代码放在第 4 行空行处(err 上方/open打开文件下方),一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。 defer f.Close() info, err := f.Stat() if err != nil { // defer机制触发, 调用Close关闭文件 return 0 } size := info.Size() // defer机制触发, 调用Close关闭文件 return size }
递归函数
所谓递归函数指的是在函数内部调用函数自身的函数。
构成递归需要具备以下条件:
注意:编写递归函数时,一定要有终止条件,否则就会无限调用下去,直到内存溢出。
宕机(panic)——程序终止运行
系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。 宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样,但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命,因此,宕机有时也是一种合理的止损方法。 当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。
package main func main() { panic("crash") }
当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用.
package main import "fmt" func main() { defer fmt.Println("宕机后要做的事情1") defer fmt.Println("宕机后要做的事情2") panic("宕机") } // 输出 // 宕机后要做的事情2 // 宕机后要做的事情1 // 宕机
宕机前,defer 语句会被优先执行
宕机恢复(recover)——防止程序崩溃
Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
panic 和 recover 的关系,panic 和 recover 的组合有如下特性:
Test功能测试函数
完善的测试体系,能够提高开发的效率,当项目足够复杂的时候,想要保证尽可能的减少 bug,有两种有效的方式分别是代码审核和测试,Go语言中提供了 testing 包来实现单元测试功能。 要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:
func TestXxx( t *testing.T ){ //...... }
编写测试用例有以下几点需要注意:
_test.go
结尾(t *testing.T)
作为参数,性能测试以(t *testing.B)
做为参数go test
命令来执行,源码中不需要 main()
函数作为入口,所有以_test.go
结尾的源码文件内以Test开头的函数都会自动执行。testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。