常见集合
1. 切片(Slice)
切片是 Go 中最基本的集合类型之一,通常用来存储多个相同类型的值。虽然切片不具备集合特性(如唯一性),你可以使用它们来存储集合数据,并通过编程逻辑来保证唯一性,可以不事先定义长度(动态化)。
在 Go 语言中,
fmt 包提供了格式化输入和输出功能。你可以使用 fmt 包中的函数来打印格式化的文本、读取输入等。例如,fmt.Println() 用于打印一行文本,fmt.Sprintf() 用于格式化字符串而不输出。
var s []type:- 这是一个切片的声明语句。s 是一个切片变量,它的类型是 []type,其中 type 是任何合法的 Go 数据类型(如 int, string, float64 等,可以替换为指定类型)。这种声明并没有初始化切片,也没有指定长度或容量。s 现在是一个 nil 切片,其长度和容量都是 0。
slice1 := make([]int, len)-- make 函数用于创建并初始化一个切片、映射或通道。对于切片,make 的签名为 make([]T, len, cap),其中 len 和 cap 分别表示切片的长度和容量。在这个例子中,make([]int, len) 创建了一个 int 类型的切片 slice1,长度为 len,并且容量等于长度(没有注明cap)(即 cap(slice1) == len(slice1))。这意味着 slice1 将有 len 个元素,所有元素都被初始化为 0(int 的零值)。
s4 := make([]int, 3, 5)-长度为3,容量为5.
截取:s[start:end]-左开右闭的截取。
append() 和 copy() 函数:
append[s1,0],增加原切片长度1个,并赋值为0.
s2 := make([]int, len(s1), (cap(s1))*2):生成一个原容量两倍的切片。
copy(s1,s2):将原切片拷贝到新切片。
2. 映射(Map)
映射是 Go 中最常用的集合类型之一,提供了键值对存储的功能,能够确保键的唯一性,适合用于实现集合的功能。
使用
struct{} 作为值,因为它占用零内存,适合表示集合中的唯一性。map[int]struct{} 是一个键为 int 类型,值为 struct{} 类型的映射。
set[1] = struct{}{}
这一行代码向映射中添加一个键值对:
set[1]表示映射中的键1。struct{}{}是值部分,由于struct{}是一个空的结构体类型,它不包含任何字段。
数组
1.数组声明
在 Go 语言中,var 关键字用于声明变量。对于数组(以及其他类型的变量),var 是声明变量的标准方式。让我们深入理解为什么在 arr1 前面需要 var。
为什么 var 是必要的
-
声明变量:
var关键字用于声明一个新变量并指定其类型。声明变量时需要确定变量的类型和可能的初始值。没有var关键字,Go 编译器无法知道你要声明一个新变量,可能会引发语法错误。
-
数组声明:
- 当你声明一个数组时,使用
var来指定数组的长度和元素类型。例如,var arr1 [3]int表示声明一个长度为 3 的整数数组。此数组的每个元素都被初始化为该类型的零值(对于int是 0)。
- 当你声明一个数组时,使用
-
区别于短变量声明:
- Go 语言还提供了短变量声明方式
:=,用于在函数内部快速声明和初始化变量。这种方式在声明时必须赋值,因此不适用于只声明不初始化的场景。 - 例如,
arr2 := [3]int{1, 2, 3}是一个短变量声明,:=自动推断类型并初始化变量。它不能用来声明只存在类型的变量,如arr1的声明。
- Go 语言还提供了短变量声明方式
数组遍历
func TestArrayTravelNormal(t *testing.T) {
arr := [...]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i ++ {
t.Log(arr[i]);
}
}
数组截取
arr[start:end]:创建一个从 start 到 end-1(左开右闭) 的新切片,其中 start 是切片的起始索引,end 是切片的结束索引(但不包含在内)。
func TestSliceArray(t *testing.T) {
arr := [...]int{1, 2, 3, 4, 5}
t.Log(arr[1:2], arr[1:3], arr[1:len(arr)], arr[1], arr[:3], arr[:]);
}
t.Log 是 Go 的 testing 包中的函数,用于在测试中记录日志信息。这些日志信息会在测试运行时输出,帮助调试和验证测试用例的结果。
字符串
与其他主要编程语言的差异
- string 是基础数据类型 不是引用或者指针类型。
- string 是只读的 byte slice ,len 函数可以计算出它所包含的 byte 数。
- string 的 byte 数组可以存储任何类型的数据(binary safe)
字符串的定义和转化:
函数
函数定义
- 一个函数可以有多个输入值和多个返回值
2.可变长参数的运用
面向对象
结构体的创建
//定义封装
type Employee struct{//type用于定义一个新的类型Employee,struct定义一个可以包含多字段的结构体
Id string //数据成员 【字段名 类型】
Name string
Age int
}
//创建和初始化
e := Employee{"0", "Bob", 20} // 第一种,分别把每个field值放进来
e1 := Employee{Name: "Mike", Age: 20} // 第二种,指定field的名字和field的值
e2 := new(Employee)// 第三种,用new关键字创建一个指向实例的指针,注意这里返回的是引用/指针,相当于e := &Employee{}
e2.Id=0 //通过指针访问实例里的成员时不需要用 ->
e2.Name="Jack"
e2.Age=30
行为(方法)定义
与其他主要编程语言的差异
// 第一种定义方式在实例对应方法被调用时,实例的成员会进行值复制
func (e Employee) String() string {
return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
} // 有声明帮助访问实例里面的数据
// 通常情况下为了避免内存拷贝使用第二种定义方式
func (e *Employee) String() string {
return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}
第一种接收者:e Employee
- 含义:
e是方法的接收者,类型为Employee。它是一个值接收者,表示方法在调用时会复制结构体的值。 - 效果:当调用这个方法时,会将
Employee实例的副本传递给方法。这意味着方法内部对e的任何修改都不会影响原始Employee实例。
第二种接收者:e *Employee
- 含义:
e是方法的接收者,类型为*Employee。它是一个指针接收者,表示方法在调用时接收结构体的指针。 - 效果:当调用这个方法时,
Employee实例的指针会被传递给方法。这意味着方法可以直接操作原始实例的数据,并且不会进行值的复制,因此更高效。
func (e Employee) String() string {
return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}
func TestStructOperations(t *testing.T){//测试函数
e := Employee{"0", "Susan", 24}//复制值
t.Log(e.String())//调用测试函数中的日志功能
//指向实例的指针也可以调用实例的方法,与通过一个类型指针的实例调用它的成员或方法不需要使用箭头符号同理
}
func (e *Employee) String() string {
return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}
func TestStructOperations(t *testing.T){
e := &Employee{"0", "Susan", 24}//构建指针
t.Log(e.String())
//指向实例的指针也可以调用实例的方法,与通过一个类型指针的实例调用它的成员或方法不需要使用箭头符号同理
}
Go语言的接口
接口的定义
type 接口类型名 interface{
say()
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个🌰,定义一个包含Write方法的Writer接口。
type Writer interface{
Write([]byte) error
}
当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。
实现接口的条件
接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
我们定义的Singer接口类型,它包含一个Sing方法。
// Singer 接口
type Singer interface {
Sing()
}
我们有一个Bird结构体类型如下。
type Bird struct {}
因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求。
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("叽叽喳喳")
}
这样就称为Bird实现了Singer接口。
func (b Bird) Sing() {} :
-
(b Bird):这里的b是接收者,类型为Bird。这个接收者表示Sing方法是Bird类型的方法。 -
Sing():方法的名字,与Singer接口中的方法名一致。该方法的实现是打印"叽叽喳喳"。 -
实现接口:因为
Bird类型的Sing方法符合Singer接口的要求(即具有一个无参数和无返回值的Sing方法),所以Bird实现了Singer接口。 -
- 接口的实现:在 Go 中,接口的实现是隐式的。你不需要显式地声明
Bird实现了Singer接口。只要Bird类型实现了接口所要求的方法,Go 会自动认为Bird实现了Singer接口。
- 接口的实现:在 Go 中,接口的实现是隐式的。你不需要显式地声明
-
实现方法:实现接口的关键是提供接口中定义的所有方法,而不是定义接口的方法。因此,接口名
Singer不会出现在方法定义中,只需要实现接口中声明的方法Sing()即可。
错误处理
GO没有异常机制
理由:trycatch使得程序变得复杂
由于GO函数支持了多值返回,通常使用参数返回的方式表示函数传递运行错误——正常返回结果的最后一个参数作为error标志
var LessError = errors.New("n should be not less than 2")
var LargegError = errors.New("n should be less than 100")
// 求斐波拉契数列
func getFib(n int) ([]int, error) {
// 限制n 的范围
if n < 2 {
return nil, LessError
}
if n > 100 {
return nil, LargeError
}
// ...
}
func TestGetFib(t *testing.T) {
// 从返回结果中判断是否有错误
if v, err := getFib(10); err != nil {
if err == LessError {
//....
}
if err == LargeError {
//....
}
} else {
t.Log(v)
}
}
实践原则
尽早暴露错误
体现在代码上:不去判断没出错,而是判断出错了(判断没出错会造成if语句嵌套,判断出错return可以避免)
go modules
1. Go Modules(go mod)
Go Modules 是 Go 语言的官方包管理系统,主要用于:
- 管理项目的依赖。
- 处理不同版本的库。
- 保证项目的构建一致性。
2. Go Modules 相关命令
初始化和设置
-
go mod init <module>:初始化一个新的模块。这个命令会创建一个go.mod文件,包含模块的路径和 Go 版本。bash 复制代码 go mod init example.com/myapp -
go mod tidy:清理go.mod和go.sum文件,移除不再使用的依赖,添加缺失的依赖。go mod tidy -
go mod verify:验证go.sum中的哈希值是否匹配项目的实际依赖。go mod verify
依赖管理
-
go get <module>@<version>:添加或更新模块的依赖,指定版本(可以是标签、分支或提交哈希)。这会修改go.mod文件和go.sum文件。go get example.com/somepkg@v1.2.3go get用于添加或更新项目中的依赖项,指定版本号、标签、分支或提交哈希来进行精确控制。
-
go list -m all:列出所有模块及其版本,包括直接和间接依赖。go list -m all -
go mod download:下载go.mod中列出的所有模块到模块缓存中,但不进行构建。go mod download -
go mod edit:直接编辑go.mod文件,可以用来添加、删除或修改模块路径和版本。go mod edit -require=example.com/somepkg@v1.2.3 -
go mod graph:打印模块依赖图,展示模块之间的依赖关系。go mod graph
查看和解决依赖
-
go mod why <module>:解释为什么需要某个模块,显示该模块的依赖关系。go mod why example.com/somepkg -
go mod why -m <module>:解释为什么需要某个模块的某个版本。go mod why -m example.com/somepkg@v1.2.3
3. go get 命令
go get 是用于下载和安装 Go 包及其依赖的命令。它有以下功能:
-
下载指定模块或包:从远程仓库下载模块代码到本地缓存,并更新
go.mod和go.sum文件。go get example.com/somepkg -
升级模块:通过指定版本号升级模块的版本。
go get example.com/somepkg@v1.2.3 -
降级模块:通过指定较低的版本号降级模块的版本。
go get example.com/somepkg@v1.1.0 -
获取最新版本:不指定版本时,
go get会默认下载模块的最新版本。go get example.com/somepkg
4. go.mod 文件
go.mod 文件是 Go 模块的核心文件,包含了模块的基本信息和依赖关系:
-
模块路径:模块的路径(通常是项目的根路径)。
module example.com/myapp -
Go 版本:项目使用的 Go 语言版本。
go 1.19 -
依赖:列出模块所依赖的其他模块及其版本。
require ( example.com/somepkg v1.2.3 anotherpkg v2.0.0 )
5. go.sum 文件
go.sum 文件用于记录所有模块及其版本的校验和,以确保依赖的正确性和一致性。它包括:
-
模块的校验和:确保下载的模块与
go.mod文件中指定的版本一致。example.com/somepkg v1.2.3 h1:abc123... example.com/somepkg v1.2.3/go.mod h1:def456...
总结
go mod是 Go 语言的模块管理工具,用于处理依赖关系和版本管理。go get命令用于添加、更新或删除模块的依赖。go mod常用命令 如init,tidy,verify,edit等,用于管理和维护模块。go.mod和go.sum文件 记录了模块的依赖和校验信息,确保构建的一致性和可靠性。
并发编程
goroutine
在 Go 里,每一个并发执行的活动称为 goroutine。
当一个程序启动时,只有一个 goroutine 来调用 main 函数,它被称为 main goroutine。新的 goroutine 通过 go 语句进行创建。
// func1(输出乱序的0-10,顺序由调度顺序决定,每个函数对应一个独立的i,线程安全)
func main(){
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Printf("i=%d\n", i)
}(i)
}
time.Sleep(10 * time.Millisecond)
}
// func2(输出全为10,线程不安全,所有输出共享最后一个i,为10)
func main(){
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(10 * time.Millisecond)
}
通道
如果说 goroutine 是 Go 程序并发的执行体,通道就是它们之间的连接。通道是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。每一个通道是一个具体类型的导管,叫做通道的元素类型。
通道channel的创建
使用内置的 make 函数可以创建一个通道,具体形式如下:
ch := make(chan int)
与 map 一样,通道也是一个使用make创建的数据结构的引用,,所以通道的零值是 nil。
通道的操作
通道主要有两个操作:发送(send)和接收(receive),二者统称为通信。发送语句中,通道和值分别在 <- 的左右两边。接收表达式中,<- 放在通道操作数前面。
ch <- x // 发送语句
x = <- ch // 赋值语句中的接收表达式
<- ch // 接收语句,丢弃结果
通道支持的第三个操作:关闭(close),它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了。
close(ch)
无缓冲通道与缓冲通道
简单make调用创建的通道是无缓冲通道(unbuffered),make还可以接受第二个可选参数,表示通道容量。如果容量是0,make创建的就是一个无缓冲通道。
ch = make(chan int) // 无缓冲通道
ch = make(chan int, 0) // 无缓冲通道
ch = make(chan int, 3) // 容量为3的缓冲通道