0、Go语言strconv包实现字符串和数值类型的相互转换 1、Slice

1.1 定义

数组
切片终止索引标识的项不包括在切片内
append()

1.2 切片的底层原理

切片的底层结构,即 reflect.SliceHeader:

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

由切片的结构定义可知,切片的结构由三个信息组成:

指针:指向底层数组中切片指定的开始位置;
长度:切片的长度;
容量:当前切片的容量;

1.3 切片的创建方式

1.3.1 从已有的数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组生成切片,代码如下:

var a  = [3]int{1, 2, 3}
var aa = a[1:2]
fmt.Println(a, aa)

其中 a 是一个拥有 3 个整型元素的数组,被初始化为数值 1 到 3,使用 a[1:2] 可以生成一个新的切片 aa,代码运行结果如下:
[1 2 3] [2]

其中 [2] 就是 aa 切片操作的结果。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与源切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

1.3.2 直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

var name []Type

其中 name 表示切片的变量名,Type 表示切片对应的元素类型。

下面代码展示了切片声明的使用过程:

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)

代码输出结果:

[] [] []
0 0 0
true
true
false

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make([]Type, Len, Cap)
TypeLenCapLen只是能提前分配空间,降低多次分配空间造成的性能问题

示例如下:

a := make([]int, 2)
b := make([]int, 2, 10)

fmt.Println(a, b)
fmt.Println(len(a), len(b))

代码输出如下:

[0 0] [0 0]
2 2
容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

1.4 注意

使用 make()函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

1.5 append()为切片添加元素

Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
容量的扩展规律是按容量的 2 倍数进行扩充
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多

1.6 切片复制(切片拷贝)

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy() 函数的使用格式如下:

copy( destSlice, srcSlice []T) int

其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

下面的代码展示了使用 copy() 函数将一个切片复制到另一个切片的过程:

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。

1.7 切片中删除元素

Go语言并没有对删除切片元素提供专用的方法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

删除的原理是从已有的切片生成新的切片。切片中删除元素

1.8 循环迭代切片

range
2、map

2.1 map的声明

声明
var mapname map[keytype]valuetype

其中:

  • mapname 为 map 的变量名。
  • keytype 为键类型。
  • valuetype 是键对应的值类型。

提示:[keytype] 和 valuetype 之间允许有空格。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。

2.2 map的创建方式

	//方式1
	var mapLit = map[string]string{}
	
	//方式2
	mapCreated := make(map[string]float32)

方式1和方式2等价。但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:

mapCreated := new(map[string]float)
mapCreated["key1"] = 4.5
invalid operation: mapCreated["key1"] (index of type *map[string]float).

2.3 K-V 一对多实现

切片
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

2.4 map元素的删除和清空

2.4.1 删除map中的元素

使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:

delete(map, 键)

其中 map 为要删除的 map 实例,键为要删除的 map 中键值对的键。

2.4.2 清空map中的所有元素

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

2.5 sync.Map(在并发环境中使用的map)

3、Go语言list(列表)
container/list双向链表
container/listNew()list
变量名 := list.New()
4、Go语言nil:空值/零值 5、Go语言make和new关键字的区别及实现原理
  • new 关键字只分配内存,当我们想要获取指向某个类型的指针时可以使用 new ;
  • make 关键字的主要作用是初始化内置的数据结构,也就是 slice、map 和 channel 的初始化。
6、函数

函数的基本组成:

func

在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

6.1 函数类型的变量

注意:
package main

import (
	"fmt"
	"strconv"
)

func main() {
	//1、调用无参、无返回值的函数
	var walkFun func()
	walkFun = walk
	walkFun()

	//2、调用有参、无返回值的函数
	var flyFun func(paramName string)
	flyFun = fly
	flyFun("鸟儿")

	//3、调用有参、有返回值的函数
	var runFun func(paramName string, paramDistance float64) (res string)
	runFun = run
	res := runFun("兔子", 500.00)
	fmt.Println(res)
}

func walk() {
	fmt.Println("walk")
}

func fly(name string) {
	fmt.Printf("%s 在飞\n", name)
}

func run(name string, distance float64) (result string) {
	result = name + ":跑了 " + strconv.FormatFloat(distance, 'f', 3, 64) + " 米"
	return result
}

6.2 匿名函数

匿名函数:没有函数名字的函数。

6.3 闭包

闭包:
(即:闭包=匿名函数+引用环境)
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改
闭包有一个特点,包含闭包的函数,它的返回类型都是函数类型,返回的实际上是匿名函数。

6.4 Go语言defer、panic、recover 详解

6.4.1 defer

  • Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

  • 处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

  • defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

7、Go语言Test功能

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:

func TestXxx( t *testing.T ){
    //......
}

编写测试用例有以下几点需要注意:

  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。
8、结构体
new&

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

8.1 实例化

8.1.1 基本的实例化形式

var ins T
Tins

8.1.2 创建指针类型的结构体来实例化

使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

ins := new(T)

其中:

  • T 为类型,可以是结构体、整型、字符串等。
  • ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。

8.1.3 取结构体的地址实例化

在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:

ins := &T{}

其中:

  • T 表示结构体类型。
  • ins 为结构体的实例,类型为 *T,是指针类型。
取地址实例化是最广泛的一种结构体实例化方式

8.2 初始化结构体的成员变量

“键值对”形式多个值的列表形式
  • 键值对:键值对形式的初始化适合选择性填充字段较多的结构体;
  • 多个值:多个值的列表形式适合填充字段较少的结构体。

8.2.1 键值对形式的初始化

ins := 结构体类型名{
    字段1: 字段1的值,
    字段2: 字段2的值,
    …
}

键值之间以:分隔,键值对之间以,分隔。

提示:结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误。

示例:

type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}

8.2.2 多个值的列表初始化

ins := 结构体类型名{
    字段1的值,
    字段2的值,
    …
}

多个值使用逗号分隔初始化结构体。

9、接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

9.1 接口声明的格式

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}

对各个部分的说明:

当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问
type writer interface{
    Write([]byte) error
}

9.2 实现接口的条件

类型 T
注意:

是通过另一个类型来实现接口中的方法的,这个类型可以是一个非接口类型,也可以是一个接口类型。一般是用结构体类型或者接口类型。

实现关系在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于 implements 的关键字。Go编译器将自动在需要的时候检查两个类型之间的实现关系。

9.2.1 接口的方法与实现接口的方法格式要完全一致

签名包括方法中的名称、参数列表、返回参数列表

9.2.2 接口中所有方法均均需被实现

全部方法

9.3 一个类型可以实现多个接口

一个类型可以同时实现多个接口,而接口之间彼此独立。

9.4 多个类型可以实现相同的接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。

9.5 Go语言实现日志系统

9.6 接口的嵌套组合

  • 在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。

  • 一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

9.7 空接口类型

空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

提示:

空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。

空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

10、Go项目的打包部署 11、并发之goroutine

11.1 goroutine

goroutine

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。

格式
go 函数名( 参数列表 )
  • 函数名:要调用的函数名。
  • 参数列表:调用函数需要传入的参数。

使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。如果需要在 goroutine 中返回数据,可以借助通道(channel),通过通道把数据从 goroutine 中作为返回值传出。

11.2 goroutine竞争状态

有并发,就有资源竞争,如果两个或者多个 goroutine 在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。

锁住共享资源
12 并发之channel

12.1 channel简介

channel通信方式我们可以使用 channel 在两个或多个 goroutine 之间传递消息

channel:是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

Go语言提倡使用通道的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。
通道是引用类型,必须使用 make 创建 channel
通道实例 := make(chan 数据类型)
  • 数据类型:通道内传输的元素类型。
  • 通道实例:通过make创建的通道句柄。

代码示例:

ch1 := make(chan int)                 // 创建一个整型类型的通道
ch2 := make(chan interface{})         // 创建一个空接口类型的通道, 可以存放任意格式
type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip)             // 创建Equip指针类型的通道, 可以存放*Equip

通道创建后,就可以使用通道进行发送和接收操作。

12.2.1 往channel里发送数据

<-
通道变量 <- 值
  • 通道变量:通过make创建好的通道实例。
  • 值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。

代码示例:

// 创建一个空接口通道
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"
fatal error: all goroutines are asleep - deadlock!

12.2.2 往channel里发送数据

使用通道接收数据同样使用<-操作符,通道接收注意事项:

  1. 通道的收发操作需在不同的两个 goroutine 间进行。
  2. 接收将持续阻塞直到发送方发送数据。
  3. 每次接收一个元素。

12.2.2.1 阻塞接收数据

data := <-ch

执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

12.2.2.2 非阻塞接收数据

data, ok := <-ch
  • data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
  • ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel进行,可以参见后面的内容。

12.2.2.3 忽略接收到的数据

<-ch
实现并发同步

12.2.2.4 循环接收channel中的数据

for data := range ch {
}

通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。
注意:在使用循环接收数据时,实际上是通过for循环一直阻塞监听channel中是否有数据,所以在程序中应该先运行接收方的goroutine程序,后运行发送方的程序。

12.3 Go语言channel超时机制

Go语言没有提供直接的超时处理机制,虽然 select 机制不是专门为超时而设计的,却能很方便的解决超时问题,因为 select 的特点是只要其中有一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。

select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。

select主要是监听读取通道中是否有数据,然后在执行case语句。

与 switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作,大致的结构如下:

select {
    case <-chan1:
    // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行该case处理语句
    default:
    // 如果上面都没有成功,则进入default处理流程
}

在一个 select 语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有如下两种可能的情况:

  • 如果给出了 default 语句,那么就会执行 default 语句,同时程序的执行会从 select 语句后的语句中恢复;
  • 如果没有 default 语句,那么 select 语句将被阻塞,直到至少有一个通信可以进行下去。