Go 基础
内置基础类型
数值类型
rune
,int8
,int16
,int32
,int64
和 byte
,uint8
,uint16
,uint32
,uint64
,其中 rune
是 int32
的别称,byte
是 uint8
的别称。
浮点数 的类型有 float32
和 float64
两种(没有 float
类型)。
复数 的类型有 complex128
(64 位实数 + 64 位虚数)和 complex64
(32 位实数 + 32 位虚数)。复数的形式为 RE + IMi
,其中 RE
是实数部分,IM
是虚数部分,而最后的 i
是虚数单位。
1 | go复制代码var c complex64 = 5 + 5i |
string
在 Go 中字符串是不可变的,但如果真的想要修改怎么办呢?
1 | go复制代码s := "hello" |
当需要对一个字符串进行频繁的操作时,谨记在 go 语言中字符串是不可变的(类似 java 和 c#)。使用诸如 a += b
形式连接字符串效率低下,尤其在一个循环内部使用这种形式。这会导致大量的内存开销和拷贝。应该使用一个字符数组代替字符串,将字符串内容写入一个缓存中。
1 | go复制代码var b bytes.Buffer |
注意:由于编译优化和依赖于使用缓存操作的字符串大小,当循环次数大于 15 时,效率才会更佳。
数组
数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。
1 | go复制代码var variable_name [SIZE] variable_type |
多维数组
1 | go复制代码var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type |
1 | go复制代码func main() { |
slice
slice
是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值。
slice
是一个结构体,这个结构体包含了三个元素:
- 引用数组指针地址;
- 切片的目前使用长度;
- 切片的容量;
1 | go复制代码// 默认是 nil |
1 | go复制代码func main() { |
1 | ini复制代码a: 0xc00011a000 |
1 | go复制代码func main() { |
1 | css复制代码arr [100 2 3] |
slice 和垃圾回收
切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存。
1 | go复制代码var digitRegexp = regexp.MustCompile("[0-9]+") |
map
1 | go复制代码// 默认是 nil,nil map 不能用来存放键值对 |
make & new
make
只能创建 slice
、map
和 channel
,并且返回一个有初始值(非零)的 T
类型,而不是 *T
。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个 slice
,是一个包含指向数据(内部 array
)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice
为 nil
。对于 slice
、map
和 channel
来说,make
初始化了内部的数据结构,填充适当的值。
内建函数 new
本质上说跟其它语言中的同名函数功能一样,new(T)
分配了零值填充 T
类型的内存空间,并且返回其地址,即一个 *T
类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型 T
的零值。
1 | go复制代码// OK |
使用 new 和 make 创建 map 时的差异
使用 new 来创建 map 时,返回的内容是一个指针,这个指针指向了一个所有字段全为 0 的值 map 对象,需要初始化后才能使用,而使用 make 来创建 map 时,返回的内容是一个引用,可以直接使用。
1 | go复制代码// 使用 new 创建一个 map 指针 |
1 | go复制代码// 使用 make 来创建并使用 map |
结论:
- 切片、map 和通道,使用
make
。 - 数组、结构体和所有的值类型,使用
new
。
iota
常量中的数据类型只可以是布尔型、数值型(整数型、浮点型和复数)和字符串型。
1 | go复制代码const ( |
error
1 | go复制代码err := errors.New("emit macho dwarf: elf header corrupted") |
1 | go复制代码type User struct { |
1 | rust复制代码Usernam or password shouldn't be empty! |
流程和函数
流程控制
switch
1 | go复制代码func main() { |
1 | arduino复制代码2、case 条件语句为 true |
for
1 | go复制代码sum := 1 |
break / continue
1 | go复制代码func main() { |
goto
1 | go复制代码func main() { |
1 | less复制代码a的值为 : 10 |
函数
变参
1 | go复制代码// 变量 arg 是一个 int 的 slice |
函数作为实参
在 Go 中函数也是一种变量,可以通过 type
来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
1 | go复制代码func main() { |
1 | 复制代码2 |
1 | go复制代码type testInt func(int) bool // 声明了一个函数类型 |
1 | ini复制代码slice = [1 2 3 4 5 7] |
闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个“内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
1 | go复制代码func getSequence() func() int { |
1 | 复制代码1 |
1 | go复制代码func add(x1, x2 int) func(int, int) (int, int, int) { |
1 | 复制代码1 3 2 |
工厂函数案例。
1 | go复制代码func main() { |
1 | 复制代码file.bmp |
defer
当函数执行到最后时,defer
语句会按照逆序执行。
1 | go复制代码func trace(s string) string { |
1 | makefile复制代码entering: b |
使用 defer 语句来记录函数的参数与返回值。
1 | go复制代码func func1(s string) (n int, err error) { |
1 | css复制代码2022/02/28 22:36:18 func1("Go") = 7, EOF |
循环内的 defer
没有执行,所以文件一直没有关闭。垃圾回收机制可能会自动关闭文件,但是这会产生一个错误。
1 | go复制代码for _, file := range files { |
defer
仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行。
1 | go复制代码for _, file := range files { |
panic & recover
panic 是一个内建函数,可以中断原有的控制流程,进入一个 panic
状态中。当函数 F
调用 panic
,函数 F
的执行被中断,但是 F
中的延迟函数会正常执行,然后 F
返回到调用它的地方。在调用的地方,F
的行为就像调用了 panic
。这一过程继续向上,直到发生 panic
的 goroutine
中所有调用的函数返回,此时程序退出。可以直接调用 panic
产生,也可以由运行时错误产生,例如访问越界的数组。
recover 是一个内建函数,可以让进入 panic
状态的 goroutine
恢复过来。recover
仅在延迟函数中有效。在正常的执行过程中,调用 recover
会返回 nil
,并且没有其它任何效果。如果当前的 goroutine
陷入 panic
状态,调用 recover
可以捕获到 panic
的输入值,并且恢复正常的执行。
1 | go复制代码func throwsPanic(f func()) (b bool) { |
main 函数 & init 函数
Go 里面有两个保留的函数:init
函数(能够应用于所有的 package
)和 main
函数(只能应用于 main package
),这两个函数在定义时不能有任何的参数和返回值。虽然一个 package
里面可以写任意多个 init
函数,但强烈建议在一个 package
中每个文件只写一个 init
函数。
Go 程序会自动调用 init()
和 main()
,所以不需要在任何地方调用这两个函数。
程序的初始化和执行都起始于 main
包,如果 main
包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt
包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init
函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main
包中的包级常量和变量进行初始化,然后执行 main
包中的 init
函数(如果存在的话),最后执行 main
函数。
import
Go 程序是通过 package
来组织的。
- 包名与文件名没有直接关系;
- 包名与文件夹名没有直接关系;
- 同一个文件夹下的文件只能有一个包名,否则编译报错;
- 只有包名为
main
的源码文件可以包含main
函数; - 一个可执行程序有且仅有一个
main
包;
1 | go复制代码// 点操作,调用的时候只需要 Println(),而不需要 fmt.Println() |
struct
方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
1 | go复制代码type Foo struct { |
1 | csharp复制代码# command-line-arguments |
看来编译器首先试着给 NewFoo()
返回的右值调用 pointer method
,出错;然后试图给其插入取地址符,未果,就只能报错了。
可以被寻址的是左值,既可以出现在赋值号左边也可以出现在右边;不可以被寻址的即为右值,比如函数返回值、字面值、常量值等等,只能出现在赋值号右边。
指针或值作为接收者
在值和指针上调用方法:可以有连接到类型的方法,也可以有连接到类型指针的方法。
1 | go复制代码type B struct { |
1 | 复制代码{1} |
将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go 编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,并不一定有太大的收获。
匿名字段、继承、重写
在 Go 中,类型就是类(数据和关联的方法)。继承有两个好处:代码复用和多态。在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫组件编程(Component Programming)。
1 | go复制代码type Skills []string |
1 | csharp复制代码Her name is Jane1 |
interface
1 | go复制代码type Phone interface { |
本文转载自: 掘金