Golang基础
Packages
同一个文件夹下的不同go源码文件,其包名必须一致。
同一个包下的源文件之间代码是透明的,不同源文件之间可以互相调用。
需要注意的是,main 包用于构建可执行文件,而其他包主要用于组织和复用代码。在一个目录中,所有的 Go 文件应该属于同一个包。包名通常与目录名一致,但这不是强制性的,因为包名是通过 package 语句来指定的。
在使用 go run 或 go build 命令时,Go 编译器会根据 package main 来识别程序的入口点。其他包的入口点则是通过导入这个包并调用其中的函数来实现的。
1 | import ( |
导出包里的东西,go中导出的内容首字母是大写的
Function
在 Go 中类型是跟在变量名之后的,至于为什么要这样参见文档Go’s Declaration Syntax
1 | func add(x int, y int) int { |
当多个连续变量共享一个类型时可以省略除最后一个参数的类型的所有类型。
1 | func add(x, y int) int { |
函数可以返回任意数量的结果
1 | func swap(x, y string) (string, string) { |
在Go语言中,函数的返回值可以被命名,这些命名会被视为在函数顶部定义的变量。这个特性对于记录返回值的含义非常有用。
1 | // 带有命名返回值的函数(sum 和 difference) |
Variables
var语句声明了一个变量列表;与函数参数列表一样,类型是最后一个。var语句可以在包或函数级别。我们在这个例子中都看到了。var声明可以包含初始化器,每个变量一个。如果存在初始化式,则可以省略该类型;变量将采用初始化器的类型。
1 | var c, python, java bool |
在函数内部,可以使用:=短赋值语句代替隐式类型的var声明。在函数之外,每个语句都以关键字(var、func等)开头,因此:=结构不可用。
1 | func main() { |
Go 的数据类型
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
变量声明可以被“分解”到块中,就像import语句一样。int、uint和uintptr类型通常在32位系统上是32位宽,在64位系统上是64位宽。当你需要一个整数值时,你应该使用int,除非你有特殊的理由使用一个有大小或无符号的整数值类型。
1 | var ( |
没有显式初始值的变量声明为零值。
零值为:
数字类型为 0,
布尔类型为 false,
以及 ""(空字符串) 用于字符串。
与C语言不同,Go语言中不同类型的项之间的赋值需要显式转换
var x int = 100
var float64 y = x //error
var float64 y = float64(x) //correct
当声明变量而不指定显式类型时(通过使用:=语法或var=表达式语法),变量的类型是从右边的值推断出来的。当声明的右边有类型时,新变量的类型是相同的:
1 | var i int |
但是,当右边包含一个无类型的数字常量时,根据常量的精度,新变量可以是int、float64或complex128:
1 | i := 42 // int |
Constants
常量声明不能使用:=隐式声明,常量像变量一样声明,但使用const关键字。常量可以是字符、字符串、布尔值或数值。数值常量是高精度值。无类型常量采用其上下文所需的类型。
const (
// Create a huge number by shifting a 1 bit left 100 places.
// In other words, the binary number that is 1 followed by 100 zeroes.
Big = 1 << 100
// Shift it right again 99 places, so we end up with 1<<1, or 2.
Small = Big >> 99
)
For Control Statement
for
Go只有一个循环结构,for循环,与其他语言不同,没有(),但是有大括号
1 | for i := 0; i < 10; i++ { |
init和post语句是可选的。
1 | sum := 1 |
去掉for的分号,for就变成了while
1 | sum := 1 |
Go 中的while(true)
如果省略循环条件,它将永远循环下去,因此无限循环是紧凑表达的。
1 | for{ |
if
和for一样,if不用使用()与for类似,if语句可以在条件之前以一条短语句开始执行。语句声明的变量只在if结束前的作用域中。
1 | func pow(x, n, lim float64) float64 { |
在if短语句中声明的变量也可以在任何else块中使用。
1 | func pow(x, n, lim float64) float64 { |
switch
每个case结束时需要的break语句在Go中是自动提供的。另一个重要的区别是Go的切换用例不需要是常量,所涉及的值也不需要是整数。
1 | switch os := runtime.GOOS; os { |
不带条件的switch与带条件的switch相同。这种构造是编写长if-then-else链的一种干净的方式。
1 | t := time.Now() |
Defer
defer语句将函数的执行延迟到周围函数返回为止。==延迟调用的参数会立即求值,但直到周围的函数返回才执行函数调用==。详细说明参见文档Defer,Panic and Recover
1 | package main |
输出:
hello
world
defer函数调用被推入堆栈。当函数返回时,其延迟调用将按照后进先出的顺序执行。
1 | package main |
输出:
counting
done
9
8
7
6
5
4
3
2
1
0
Pointers
注意:指针作为参数,只是把指针也就是内存地址复制了一份,相当于把地址作为值传递给了函数
Go语言支持指针。指针保存了一个值的内存地址。
类型*T是指向T类型值的指针,其零值是nil。
1 | var p *int |
&运算符用于生成其操作数的指针。
1 | i := 42 |
*运算符表示指针的底层值。
1 | fmt.Println(*p) // 通过指针p读取i的值 |
这被称为“解引用”或“间接引用”。
与C语言不同,Go语言中没有指针算术操作。
go语言对指针做了优化,结构体用指针可以直接获得结构体字段值,不需要*,但是基本类型变量的赋值仍然需要*
1 | p := &Person{ |
Struct
1 | type Vertex struct { |
使用点访问结构域,如Vertex.X
当我们有一个结构体指针p时,可以通过(*p).X的方式访问结构体字段 X。然而,为了简化语法,Go语言允许我们直接使用 p.X 的方式来访问字段,而无需显式地进行解引用。
这种语法糖的设计使得访问结构体字段更加直观,使代码更清晰,减少了冗余的符号,提高了代码的可读性。这也是Go语言在语法设计上追求简洁和清晰的一部分体现。
1 | package main |
1 | var ( |
结构体嵌套
1 | import "fmt" |
Arrays
定义
1 | var a [10]int |
Slices
注意切片a[1,4]是半开的:[1,4)
1 | func main() { |
输出:[3 5 7]
注意
- 切片不存储数据:
切片本身并不直接存储任何数据,而只是对底层数组的一个描述,指示了数组的一部分。
切片包括一个指向数组的指针、切片的长度和容量。 - 切片修改底层数组元素:
当你修改切片中的元素时,实际上是修改了切片所引用的底层数组的对应元素。
例如,如果有一个切片 s,通过 s[0] = 42,实际上修改了底层数组的第一个元素。 - 底层数组共享:
如果有多个切片共享同一个底层数组,当一个切片修改底层数组的元素时,其他切片也会看到这些变化。这是因为它们都指向同一个底层数组。这种共享使得数据在不同切片间共享,但也需要小心避免潜在的副作用。
1 | func main() { |
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
Slices literal
- 数组字面量:
数组字面量是一种用于创建数组的语法。它包括数组的长度和具体的元素值。
例如,[3]bool{true, true, false} 是一个包含3个布尔值的数组字面量。 - 切片字面量:
切片字面量类似于数组字面量,但省略了长度。它使用[]来创建一个切片,而不是数组。
例如,[]bool{true, true, false} 是一个切片字面量,它引用了与上述数组字面量相同的数组。 - 切片引用数组:
切片字面量创建一个切片,该切片引用一个底层数组。这个底层数组可以是通过数组字面量创建的,也可以是已经存在的数组。
1 | func main() { |
在这个例子中,sliceLiteral 是通过切片字面量创建的,它引用了一个与 arrayLiteral 相同的底层数组。切片字面量省略了长度,因为切片的长度是根据其引用的数组的长度动态确定的。
Struct的切片字面量
1 | s := []struct { |
Slices可以省略前后区间边界
1 | func main() { |
Slice length and capacity
len(s)表示切片的长度,cap(s)表示切片对应的底层数组的真实元素个数
1 | func main() { |
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
Creating a slice with make
Go语言中使用内置的make函数创建切片的方法,特别是用于创建动态大小的数组。
使用 make 函数创建切片:
使用make函数可以创建指定长度和容量的切片。make函数返回一个切片,该切片引用一个被分配的零值数组。
例如,a := make([]int, 5) 创建了一个包含5个整数的切片,切片的长度和容量都为5。
指定容量:
如果想要指定切片的容量,可以向make函数传递第三个参数,该参数表示切片的容量。
例如,a := make([]int, 5, 10) 创建了一个长度为5、容量为10的切片。长度表示切片当前包含的元素数量,容量表示底层数组的大小。
切片(slices)可以包含任何类型的元素,包括其他切片。
1 | // 切片包含整数切片 |
Appending to a slice
文档
Slice: usage and internals
append function
Go语言中,通常使用内置的append函数向切片添加新元素。append函数的签名如下:
1 | func append(s []T, vs ...T) []T |
s 是类型为T的切片,它是第一个参数,代表要追加元素的目标切片。
vs 是类型为 T 的可变参数,代表要追加到切片的元素。
append 函数的返回值是一个包含原始切片所有元素以及提供的新元素的切片。
如果切片 s 的底层数组不足以容纳所有给定的值,append函数将分配一个更大的数组,并返回一个指向新分配数组的切片。
1 | func main() { |
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]
//这儿之所以cap为6是因为append函数分配了一个更大的底层数组
Range
这段文字描述了在Go语言中, for 循环的 range 形式用于遍历切片或映射。当在切片上使用 range 时,每次迭代都会返回两个值。第一个值是索引index,第二个值是该==索引处元素的副本。==
1 | var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} |
使用 for 循环的 range 形式时,可以通过使用下划线 _ 来省略不需要的索引或值。
1 | for i, _ := range pow { |
如果只关心索引而不需要值,可以省略第二个变量。
1 | for i := range pow { |
Maps
Go语言中关于映射(map)的一些基本概念:
映射的定义:
映射用于将键(keys)映射到对应的值(values)。每个键必须是唯一的,而值可以重复。
在Go语言中,映射使用 map 关键字定义。例如:map[keyType]valueType。
映射的零值:
映射的零值是 nil。一个 nil 映射既没有键,也不能添加键值对。
在声明映射时,如果没有显示初始化,它的零值就是 nil。
使用 make 函数初始化映射:
使用内置的 make 函数可以创建一个指定类型的映射,并进行初始化,使其准备好使用。
make 函数的语法为 make(map[keyType]valueType)。
1 | type Vertex struct { |
{40.68433 -74.39967}
m[“Bell Labs”] = Vertex{
40.68433, -74.39967,
}
为什么-74.39967后面还要跟逗号
这是Go语言的一种语法约定。这样做的好处是,在添加、删除或重新排列结构体字段时,更容易生成和维护代码,因为不需要关心最后一个字段是否有逗号。所以,这里的逗号是Go语言的语法允许的,它使得在结构体定义中添加新字段更加方便。
Map literals
必须指定键值
1 | type Vertex struct { |
声明时可以省略类型名,注意作用域在哪儿
1 | type Vertex struct { |
Mutating Maps
Go 语言中的 map ,你可以执行以下基本操作:
插入或更新元素:
1 | m[key] = elem |
这行代码将在 map m 中插入或更新键 key 对应的元素为 elem。
检索元素:
1 | elem = m[key] |
这行代码将从 map m 中检索键 key 对应的元素,并将其赋值给 elem。
删除元素:
1 | delete(m, key) |
这行代码将从map m中删除键key对应的元素。
检查键是否存在:
1 | elem, ok = m[key] |
如果键key存在于 map m 中,ok 的值为 true。如果不存在,ok 的值为 false。
如果键不在map中,那么 elem 将是 map 元素类型的零值。
注意:如果 elem 或 ok 尚未声明,你可以使用短声明形式:
1 | elem, ok := m[key] |
Function values
如果函数返回值定义了可以不需要在return语句后写明:
1 | func adder(a, b int) (sum int) { |
多个参数
1 | func adder(items ...int) (sum int) { |
Go语言中,函数是值。它们可以像其他值一样传递,作为函数的参数和返回值使用。
1 | import ( |
13
5
81
函数一等公民特性
函数可以作为值赋值给变量
1 | //上面的加法函数 |
函数名加()是函数调用,作为参数只能是函数名
1 | func calSum() { |
This is Sum function
Function closures
Go中的函数可以是闭包。闭包是一个函数值,它引用了其外部作用域的变量。该函数可以访问和修改引用的变量,从这个意义上说,该函数被“绑定”到这些变量上。
1 | import "fmt" |
注意这儿add函数的输出,第一次调用sum为1了
1 | import "fmt" |
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
Methods
Go语言没有类,但可以在类型上定义方法。方法是带有特殊接收器参数的函数,接收器出现在函数关键字和方法名之间的参数列表中。
以下是一个示例,展示了一个具有名为v的类型为Vertex的接收器的Abs方法:
1 | package main |
在这个例子中,Abs方法有一个接收器 v,其类型为Vertex。这使得我们可以通过使用点符号调用该方法来对Vertex类型的实例进行操作,就好像它是一个类的方法一样。这种方式让我们可以将相关的方法与数据结构关联起来,实现一种轻量级的面向对象编程风格。
注意:Methods只是一个带有接收器的函数
可以在非结构体类型上声明方法,只要该类型是在同一个包内定义的。
1 | // MyFloat 自定义浮点数类型 |
Pointer receivers
(1)值接收器和指针接收器,指针接收器比较常用
数据较大,需要改变原始对象的值是用指针传递
1 | import ( |
50
//如果去掉*,输出值为5
(2)现在,将method改为function
1 | package main |
50
正如上面所说,method只是带有接收值的函数
你可能注意到了,在上面(1)中,v.scale(10)是被允许的,而在(2)中必须要使用scale(&v,10),在(1)中,如果p := &v;p.scale(10)也是被允许的。对于语句v. scale(10),即使v是一个值而不是指针,带有指针接收者的方法也会被自动调用。也就是说,为了方便起见,Go将语句v.Scale(5)解释为(&v).Scale(5),因为Scale方法有一个指针接收器。
Interfaces
接口是Go语言中实现多态的关键机制之一,它提供了一种灵活的方式来设计可扩展的和可复用的代码。通过接口,代码可以更加通用,同时保持清晰的结构和可维护性。
在Go语言中,接口(interface)是一种抽象类型,用于定义一组方法的集合。接口定义了对象的行为,但不提供对象的实现。对象实现接口时,必须实现接口中定义的所有方法。这种方式使得接口成为实现多态的关键机制之一。
主要特性
接口定义: 接口通过一组方法签名来定义,方法签名只包含方法的名称、参数列表和返回值列表。一个对象只要实现了接口中定义的所有方法,就被认为是实现了该接口。
1 | // 接口定义 |
接口实现: 一个对象只要实现了接口中定义的所有方法,就被认为是实现了该接口。在Go中,不需要显式声明对象实现了某个接口,只要对象满足接口的方法签名,它就被认为是实现了该接口。
1 | // 类型实现接口 |
接口变量: 接口变量可以持有任何实现了接口的对象。这种特性使得可以编写更加通用和灵活的代码。
1 | var myInterfaceVar MyInterface |
接口组合: 在Go中,接口可以被组合成一个新的接口。这种方式允许我们将多个小的接口组合成一个更大的接口,从而实现更灵活的代码设计。
1 | // 接口组合 |
在Go语言中,如果接口内部的具体值为nil,调用该接口的方法时,将使用一个nil的接收器。在其他一些语言中,这可能会触发空指针异常,但在Go语言中,通常会编写能够优雅地处理带有nil接收器的方法。
1 | package main |
在Go语言中,一个nil的接口值既不包含值也不包含具体类型。
当在nil接口上调用方法时,会触发运行时错误,因为在接口元组内部没有类型信息,无法确定调用哪个具体方法。
1 | package main |
空接口:
Go语言中的空接口(empty interface)。空接口是一种接口类型,它没有任何方法,具体的定义是interface{}。
空接口可以包含任何类型的值,因为每种类型都实现了至少零个方法。因此,空接口是一种非常灵活的类型,可以用来处理未知类型的值。
1 | import "fmt" |
(<nil>, <nil>)
(42, int)
(hello, string)
类型断言(Type assertions)
Go语言中的类型断言(type assertion)。类型断言用于访问接口值的底层具体值。
1 | t := i.(T) |
这个语句断言接口值 i 包含具体类型 T,并将底层的 T 值赋给变量 t。
如果i不包含T类型,该语句将引发 panic。
1 | t, ok := i.(T) |
如果i包含 T,那么 t 将是底层值,ok 将为 true。
如果不包含,ok 将为 false,t 将是类型 T 的零值,==不会引发 panic。==
Type switches
类型switch类似于常规的switch语句,但在类型switch的情况下,==情况指定的是类型(而不是值)==,这些类型将与给定接口值持有的值的类型进行比较。
1 | import "fmt" |
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!
I don't know about type int32!
Stringers
在Go语言中的 fmt 包中定义的 Stringer 接口。Stringer 接口有一个方法 String,该方法返回一个字符串。这个接口的存在允许类型自行描述自己的字符串表示形式。==可以理解为java的toString方法==
Stringers接口:
1 | type Stringer interface { |
实例:
1 | package main |
Errors
在Go中,错误状态通常使用实现了 error 接口的错误值表示。error 接口有一个 Error 方法,该方法返回一个描述错误的字符串。与fmt.Stringer类似,fmt 包在打印值时会寻找 error 接口,这使得错误可以被直观地输出。
接口:
type error interface {
Error() string
}
实例
1 | package main |
error为nil表示没有错误,不为nil则有错
1 | import ( |
at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
平方根测试Error
1 | package main |
为什么对e进行强制类型转换
具体来说,这是因为在 fmt 包中,对 error 类型的值的格式化是通过调用 Sprintf 函数实现的,而 Sprintf 函数会使用 fmt.Sprint 函数。当 fmt.Sprint 遇到实现了 Stringer 接口的类型时,它会调用该类型的 String 方法,而对于 error 接口,它调用的是 Error 方法。
在这种情况下,调用==fmt.Sprint(e) 实际上变成了 fmt.Sprint(e.Error()),然后又会调用 Error 方法。这导致了一个无限递归,因为每次调用 Error 方法都会再次触发 fmt.Sprint(e),而这又会导致再次调用 Error 方法,形成一个循环。==
通过将 e 转换为 float64(e),我们在 fmt.Sprint 中得到的是基础类型 float64 的字符串表示形式,而不是再次调用 Error 方法。这样就避免了递归循环,确保了错误字符串的正确生成而不导致栈溢出。
==同理Print等函数都会导致这个问题==
Readers
接口:
1 | // io.Reader 接口定义 |
io.Reader 接口包含一个 Read 方法,用于从数据流中读取数据并将其填充到给定的字节切片中。Read方法返回被填充的字节数以及可能的错误。当数据流结束时,它返回一个io.EOF错误。
1 | package main |
Read 8 bytes: Hello, G
Read 2 bytes: o!
Read 0 bytes:
(1)实现一个发出无限 ASCII 字符“A”流的 Reader 类型。
1 | type MyReader struct{} |
(2)解压缩rot13压缩的数据流(每个字符往后挪13位再%26)
一个 io.Reader 包装另一个 io.Reader,以某种方式修改流。
1 | import ( |
Generics(泛型)
类型参数
1 | func Index[T comparable](s []T, x T) int |
Index: 这是一个函数名,表示这是一个用于查找索引的函数。
[T comparable]: 这是类型参数部分,用于表示函数可以在多种类型上工作。T 是一个类型参数,comparable 是一个内建的约束条件,表示 T 必须是可比较的类型。可比较的类型意味着可以使用==和 != 运算符对该类型的值进行比较。
(s []T, x T) int: 这是函数的参数列表和返回类型。s 是一个包含类型 T 的切片,x 是一个类型为 T 的值。函数返回一个 int,表示找到的元素在切片中的索引。
1 | // Index returns the index of x in s, or -1 if not found. |
2
-1
利用泛型创建可以存储任意类型的结构体
单链表(头插法)
1 | package main |
单链表(尾插法)
1 | import "fmt" |
Concurrency
Goroutines
要让两个进程执行分先后需要用到sync.WaitGroup,同步控制
1 | import ( |
Channels
Channel是一种类型化的管道,您可以通过它使用通道运算通道操作:
<- 是通道操作符,用于发送和接收数据。
ch <- v 将值v发送到通道ch中。
v := <-ch 从通道 ch 中接收一个值,并将其赋值给变量 v。
创建通道:
通道在使用前必须先创建,使用make函数。
ch := make(chan int) 创建一个整数通道。
默认阻塞:
默认情况下,发送和接收操作会阻塞,直到另一侧准备好。这允许 goroutine 在没有显式锁或条件变量的情况下同步执行。
1 | ch := make(chan int) |
1 | func sum(s []int, c chan int) { |
输出
-5 17 12
或者: 17 -5 12
Buffered Channels
带缓冲的通道在某种程度上类似于队列。它们都是一种用于在多个 goroutine 之间安全传递数据的机制,并且具有先进先出(FIFO)的特性。
1 | package main |
output:
1
2
Range and Close
Range 和 Close
发送者可以关闭一个通道以表示不再发送更多的值。接收者可以通过为接收表达式分配第二个参数来测试通道是否已关闭:
1 | v, ok := <-ch |
当没有更多的值可接收且通道已关闭时,ok 将为 false。
通过 for i := range c 循环,可以重复从通道接收值,直到通道关闭。
注意:只有发送者应该关闭一个通道,永远不要由接收者关闭通道。在关闭的通道上发送将导致 panic。
另一个注意点:通道不像文件;通常不需要关闭它们。只有在接收者必须被告知没有更多的值即将到来时,比如为了终止一个 range 循环,才需要关闭通道。
1 | import ( |
0
1
1
2
3
5
8
13
21
34
Select
select 语句会阻塞,直到其其中一个 case 可以执行,然后它会执行该 case。如果有多个 case 准备就绪,它会随机选择其中一个执行。
等待多个通信操作: select 允许在多个通信操作上等待,这可以是通道的发送和接收,以及 default 分支,用于处理非阻塞的情况。
随机选择: 如果有多个 case 准备就绪,select 会==随机==选择其中一个来执行(防止饥饿)。这样可以在多个并发操作中做出随机选择,而不是按照固定的顺序执行。
阻塞: 如果没有任何 case 准备就绪,且没有 default 分支,select 语句将阻塞,直到其中一个 case 可以执行。
1 | import ( |
输出 Received: World
因为ch1最先准备就绪
斐波那契
1 | import "fmt" |
Default Selection
为了处理这种可能出现的阻塞情况,可以使用default case。在这个例子中,如果接收操作会阻塞(也就是说,如果c是空的,或者发送操作还没有准备好),那么就会执行default case的代码块。这样就可以避免阻塞,并且继续执行其他代码。
1 | select { |
1 | import ( |
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
BOOM!
sync.Mutex(互斥量)
通过sync.mutex 包的Lock和Ulock函数来加锁和解锁。
示例:
1 | import ( |
Go 中的并发操作是异步执行的,也就是说,main 函数和 go c.Inc("somekey") 这些 goroutines 是并行执行的。当 main 函数执行完毕时,它可能会在所有的增加操作完成之前退出,导致我们无法获取正确的计数结果。
通过使用 time.Sleep(time.Second),我们在 main 函数中暂停了一秒钟,给其他的 goroutines 足够的时间来执行它们的工作。这是一种简单而不太推荐的等待所有 goroutines 完成的方法。在实际的应用中,更好的方法可能是使用 sync.WaitGroup 或者类似的同步机制来等待所有的 goroutines 完成。这样可以确保在程序结束时,所有的并发操作都已经完成,而不是通过硬编码的睡眠时间。





