Go 基本概念速记

date
Mar 20, 2022
slug
golang-quick-start
status
Published
tags
Go
summary
go 基本概念汇总
type
Post

基本变量

基本数据类型
没有明确初始化的变量都默认有个零值
 
变量
通过 var 声明变量,相同类型变量可以放一排,然后最后指定类型
按位置顺序赋值
 
全局变量和局部变量
短变量声明 := 只有在方法内才可以使用,称为局部变量 ,方法外声明的只能用 var 定义,称为全局变量
 
常量
定义常量使用 const 关键字,且不能使用 := ,只能用 var,常量类型只允许布尔型、数字型(整数型、浮点型和复数)和字符串型
 
字符
go 没有单独的字符类型,可以用 byte 代替,不过赋值进去的是字符,而用 %v 输出的是数值,因为 byte 就是 int8 的别名,想要输出字符,需要用 %c 来格式化
同理反过来说也可以把一个 uint8 按照字符输出
 
字符串
可以使用 `` 来表示字符串,该字符串内部的字符会原样输出
大部分字符串常用函数都在 strings 包中,例如 Index Replace Split Trim HasPrefix HasSuffix
 
类型转换
必须显示转换,不存在自动的隐式转换
类型推断的精度取决于赋值的量本身的精度
 
数值转字符串
可以使用 fmt.Sprintf 配合不同的格式化占位符做展示,占位符列表在这里 https://pkg.go.dev/[email protected]#hdr-Printing
常用的有
 
字符串转数值
可使用标准库 strconv 转换字符串到其他类型
转数值时,ParseInt 第一个参数是字符串,第二个参数 base 是指明原字符串采用几进制,第三个参数 bitSize 表示原字符串可以用多少位 bit 存下,如果实际字符串长度超出传入的 bitSize 数组,则会发生溢出,转换后的结果会将溢出部分舍弃,只保留bitSize 个 bit 的最大值,最后再转成 int64 返回,而溢出的具体信息放在返回值 e 中。
浮点型转换,注意精度问题
 
函数
定义一个函数
如果参数类型相同,可以省略前面几个参数类型,只保留最后的类型
多个返回值
 

for if else switch

for
go 只有一种循环,for 有三种组成部分,初始化语句,条件表达式,后置语句,中间用 ; 分隔,初始化和后置语句不是必须的。for 语句没有小括号,但是循环体需要 {}
省略初始化和后置语句
省略分号,就变成了 while 循环
再省略掉条件语句,就变成了 while 死循环
 
for range
range 子句可以遍历字符串,数组,切片,字典或 channel
注意在遍历字符串时,for range 会按照字符遍历,但索引值会按照字节数递增。
 
 
if 条件
if 语句不需要小括号,但必须有 {}
if 支持一个初始化语句,在判断之前执行,初始化的变量只在当前 if 作用域内有效
 
switch case
同样支持初始化语句,go 只会运行第一个匹配的 case,不会继续向下执行,即相当于每一个 case 自带了一个 break,另外 cases 的值可以是任意类型而不必是数字,这两点跟其他语言不一样。
不写 switch condition 就相当于 switch true,也就是判断下面哪个 case 是 true,相当于变相的 if else 组合
 

pointer、array、slice、map

指针
p = *int 定义一个指针,存的是 int 类型变量的地址。
p = &i 将变量 i 的内存地址赋值给指针 p
&i 取出变量 i 的内存地址
*p 取出指针 p 指向的变量的数值,也称为间接引用 dereferencing 或重定向 indirecting
*p 操作也可以称为取引用,然后对该引用做处理,直接反映到原始变量上
 
map 映射
使用 make 创建一个 map:make(map[<key的类型>]<值的类型>)
映射的零值为 nilnil映射既没有键,也不能添加键。
map的增删查改
更新 m[k] = v
查询 m[k]
删除 delete(m, k)
查询一个key
如果key不存在,则返回map元素的0值,并且第二个返回值是false。
如果key已存在,则返回该值,并且第二个返回值是 true
 
数组
[n]T 是一个含有 n 个 T 类型元素的数组,var a [10]int
数组的长度是固定的,声明之后就不可改变。
 
切片 slice
[]T 表示一个元素类型为 T 的切片。切片是数组的一个视角,可以动态从数组中切分出一块连续元素。
切片初始化声明跟数组类似,只是不需要指定长度,或者使用 make 函数生成
make 函数可以指定长度和容量
 
a[low : high] 切片结果中包括 low 元素,排除 high 元素
 
切片就像是数组的引用,通过切片做出的修改,会直接反映到数组以及其他切片上。
 
数组和切片的定义:
a := [3]bool{true, true, false} 指定长度就是数组
b := []bool{true, true, false} 生成一个数组,然后用 b 作为这个数组的引用,不显式返回这个数组,而是直接用 b 这个切片。
 
切片的默认边界,默认下界是0,默认上界是该slice的长度,注意并不是slice下面的数组的长度
s[:] 相当于当前切片全都要。
涉及到长度,都跟切片有关。涉及到容量,都跟底层数组有关
 
切片的长度与容量
长度是指当前切片中有几个元素
容量是指当前切片中第一个元素开始数,到底层数组最后一个元素的个数。这点需要注意。
  • 长度:切片开始元素 至 切片末尾元素的元素个数,使用 len() 函数获取长度
  • 容量:切片开始元素 至 数组末尾元素的元素个数,使用 cap() 函数获取容量
 
切片的零值
nil 切片长度和容量都是 0,且没有底层数组
 
使用 make() 创建 slice
make 三个参数,第一个指定创建的类型,这里指定的是切片类型,第二个指定切片的长度,第三个指定容量,没有指定容量则默认跟长度一样
a := make([]int, 5) // len(a)=5
 
切片的切片,二维切片
 
append 函数向切片追加
当切片底层的数组不够大时,会分配一个更大的数组,然后返回的 slice 会指向新的数组
 
指针和引用的区别
指针也是一个变量,需要额外的内存空间存储这个指针变量,指针变量中存储的是真正变量的内存地址。
引用则是对象的一个别名,不单独存储。
通过引用访问源对象属于直接访问,通过指针访问源对象属于间接访问,因为需要先找到地址,再通过地址找到对象。
引用可以理解为被封装过的,功能更少的指针。
 
数组和切片细节
a := [3]bool{true, true, false} 指定长度就是数组
b := []bool{true, true, false} 没有长度就是切片
数组
Go’s arrays are values. An array variable denotes the entire array; it is not a pointer to the first array element (as would be the case in C). This means that when you assign or pass around an array value you will make a copy of its contents. (To avoid the copy you could pass a pointer  to the array, but then that’s a pointer to an array, not an array.) One way to think about arrays is as a sort of struct but with indexed rather than named fields: a fixed-size composite value.
数组本身是一个整体,是一个完整的对象,而不是像 C 语言中,一个数组实际上是这段连续内存的第一个元素的开始地址。所以传递 go 的数组的时候,实际上传递的都是值,也就是会复制一份内容出来,这也就是所谓的值传递,为了避免出现数据的复制,可以手动获取指针并传递指针。
数组可以看作是一个 struct,是一个整体,是一个支持索引访问 struct。
数组的语法 literal
 
切片
切片的语法跟数组类似,区别是没有指定元素数量
另外切片还可以通过 make 创建
切片的零值是 nil,对于一个 nil 的切片,其长度和容量都是 0。
 
切片是数组的描述符号
切片是数组的描述符号,他由一个指向底层数组的指针、长度和容量组成
notion image
 
在切片上再次切片
notion image
一个数组上的多个切片,修改数据,实际上都会反映到底层数组上,多个切片会互相看到修改。
所以在切片上再次切片,只是多了一个原底层数组的又一个切片。
下面切片 e 从切片 d 切分来,之后在 e 上修改,d 也能看到修改的数据,因为本质上修改的就是底层数组。
notion image
 
copy 操作
下面代码是申请一个更大容量的切片,把老切片内的数据复制到新切片里
这个过程跟内置的 copy 函数一样的过程。
 
append 操作
将一组数据,追加到切片末尾,过程就是把要追加的数据放入切片,前提是如果追加的元素个数超过了切片剩余容量,那么先扩容,再copy原有数据,最后追加新数据。
go 中内置的函数 append 就是干这个事儿的
使用 b… 解开切片 b
 
filter 操作
传递一个切片和一个函数,遍历给定的切片,执行函数
 
小切片持有大数组问题
切片是数组的描述符号,或引用,也就是说,只要数组还有切片在引用它,那么他就无法被 garbage collector 回收掉,即使切片只需要很少一部分数据,而整个数组都不能被回收,都需要完整的放在内存中。
比如加载一个大文件,然后寻找里面第一组连续的数字并返回。这段代码会导致这个大文件一直被引用,无法释放。
这时候就可以使用一个小的全新的切片来保存需要的数据,然后返回这个新切片。
至于写法,这里写的是用 copy 来做,也可以使用 append,是一样的。

defer、panic、recover

defer 关键字兜底逻辑
defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.
延迟执行,直到函数返回之后执行被 defer 的语句。被 defer 的函数调用其参数会立即求值,但函数需要等到整个函数都执行完才会调用。
多次 defer,每一个 defer 都相当于压栈操作,最后执行的时候按照后进先出的出栈顺序执行。
defer 三个特点:
  • 被 defer 的函数的参数会立即求值
    • defer 的调用会压入栈内,在最后执行时按照后进先出顺序执行
      • defer 的函数体中可以读取原函数的名字参数,并且还可以给其赋值。这个特点便于修改函数的错误返回值,比如官方 json 库使用 defer 结合 panic recover 来捕获递归调用中出现的异常,在最外层统一处理错误并返回
        defer 常用于打开文件后记得关闭文件、加锁之后记得解锁等等这种需要回收资源或是需要收尾的工作。
        官网blog介绍 defer panic recover :https://go.dev/blog/defer-panic-and-recover
         
        panic 抛出异常
        Panic  is a built-in function that stops the ordinary flow of control and begins panicking . When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.
        有点类似异常的意思,出现异常时调用 panic(xxx) 会中止当前函数的流程,而当前函数设置的 defer 则会逐一执行,然后函数返回给调用方,对调用方来说也相当于触发了 panic,一路向上抛出,直到有一个函数使用 recover 捕获 panic,至此不再向上抛出。
         
        recover 捕获异常
        Recover  is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
        recover 能够接到 panic 的内容,比如函数调用 panic(123),上层函数 r := recover() 可以接到 这个 123。如果没有 panic 则 r = nil。recover 后,panic 就不会再向上抛出了,相当于捕获住了。
        recover 只能在 defer 关键字作用域内有效。
        defer panic recover 组合

        function closure

        用闭包实现 Fibonacci
         

        struct method interface

        结构体定义
        结构体声明变量,通过 struct_name {x,y,z} 逐一赋值,或者指定每一个 k v 来赋值
        结构体访问,通过 struct_name.field_name 访问
         
        结构体指针
        如果一个指针 p 指向一个结构体 Vertex,理论上访问 Vertex 的属性 X,需要用 (*p).X,但 go 也支持简便的隐式间接引用,直接用 p.X
         
        给结构体添加方法
        理论上来说不光能给结构体添加方法,实际上可以给任何类型添加方法,而结构体是通过 type struct 定义出来的一种类型,所以能够为其添加方法也就很合理了。
        在 func 关键字后跟着一个类型声明,称之为 receiver,表示该方法是某类型特有方法。
        方法调用时相当于将当前类型的实例传给方法前面的 receiver,具体来说下面例子中,就是将 mt 传给了方法定义中的 mt receiver。
        默认调用方法都是通过值传递的方式,也就意味着在方法内修改变量内容不会对外产生影响,想改变内部的值需要传递引用, 即 method2
        结构体继承
        go 本身不支持继承,不过可以使用组合模式来实现继承的效果。
        通过结构体匿名嵌入的方式实现继承,Dog 中嵌入一个匿名的 Animal 的结构体,实现 Dog 继承 Animal 的属性和方法。
        通过 dog 调用时可以省略中间的匿名结构体
        Animal.go
        Dog.go
        通过在 Dog 的属性中嵌入一个 Animal 匿名结构体,实现继承关系
        使用时,初始化一个 Dog 实例,初始化时需要传入一个 Animal 实例,通过 dog 实例调用 Animal 的方法时可以省略中间匿名结构体
        嵌入继承的不完备性,即多态无法体现
        在 main.go 中增加一个调用:
        这说明 Dog 中的 Sound 方法没有被调用。
        使用 interface 来实现继承
        使用 interface 定义一组方法,任何结构体只要实现了 interface 中所有方法,就被视为实现了这个接口。
        然后再单独建立一个函数,专门接收该接口类型为参数,这样就可以处理各种实现了该接口的类型,实现多态。
         
        类型断言
        判断某个接口类型的变量是否能转成某个具体类型
        断言常见的使用方式是结合 switch 判断针对不同类型做不同业务
         
        类型的 method 方法
        go 没有类,可以通过在 type 类型上定义方法的方式来实现类的效果,一般来说,
        1. 先是通过 struct 定义一个结构体
        1. 再声明这个结构体为一种 type 类型
        1. 然后定义函数,函数定义中指定一个 receiver 接受者为该类型
        1. 如此一来就定义了一个结构体类型,且该类型具有某个方法
        注意 go 支持的是在 type 上定义 method,而不是在 struct 上定义。
        方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
        在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
        方法 method 仅仅就是一个加上了接受者 receiver 参数的函数,下面这个例子中,同样是 Abs 函数,换成了用函数方式调用
        注意,定义 method 并不是只能在 struct 上,而是 type上,只要是 type 就可以,比如 Abs 方法定义在类型 MyFloat 上,f 方法定义在类型 fdfdf 上
         
        值接收者 value receiver 和指针接收者 pointer receiver
        如何理解接受者?定义一个函数,这个函数是给谁定义的?这就是接受者的意思。
        接收者分两种
        • 值接收者 value receiver
        • 指针接收者 pointor receiver:在定义接收者时,在类型前面加 *,使 v 变成一个指针
        如果去掉了 *,变成值接收者,那么修改的 Vertex 将是一个副本,就像普通函数参数那样
         
        编译器自动处理
        如果一个函数接收一个指针类型,那么调用时必须通过指针调用
        而方法中,如果定义的是指针接收者,那么调用时,既可以使用值调用,也可以使用指针调用:
        也就是说,如果一个方法的接收者是个指针,即方法定义好之后是给指针使用的,那么使用时既可以通过指针调用,也可以通过普通值调用,因为在背后,编译器自动将值调用 v.Scale(5) 编译成了 (&v).Scale(5) 。
        反之也成立:
        p.Abs() 被解释成了 (*p).Abs()
        使用指针接收者的原因有二:
        • 首先,方法能够修改其接收者指向的值。
        • 其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
        一般来说对于一个类型,不应该混用值和指针接收者,而是二选一。
         
        interface type 接口类型
        接口是一组方法的集合,接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。过程中不需要显式使用 implement 实现什么,这叫做隐式声明。
         
        接口在引用值类型和指针类型时的区别:
        下面接口 a 指向 f 和 &v 都可以,因为他们都实现了 Abs 方法,也就隐式实现了接口 Abser,而 v 并没有实现 Abser 方法,这里注意 Abser 方法的接收者是 *Vertex,而不是 Vertex,即接收者是一个Vertex指针,而不是Vertex 值。
        反过来说就是 *Vertex 实现了 Abser,而 Vertex 并没有。
         
        接口值 interface value(应该是指面向接口编程)
        接口值可以被看做是一个由 value 和一个具体类型组成的元组,一个接口值持有一个特定底层具体类型的值。
        调用接口值时,就是执行元组中某个类型的那个方法。
        这句话说的应该就是面向接口编程的执行过程,一个 Interface i = 实现了该接口的类型的实例,
        调用时 i.xxxFunc() ,其实是调用的该实例的 xxxFunc()
        下面代码中, var i I ,定义 i 是一个接口 I 的值,i 可以指向所有实现了接口 I 的类型。
        i 可以看成是 (&T{"Hello"}, *T) 这样一个元组
         
        下面描述的是接口值 i 直接指向一个空的 *T,也就是 i 的元组中,值那个部分是 nil,不过这样也是可以编译的。
         
        空接口用来处理未知类型的值,或叫任意类型的值
        interface{}
        空接口可以指代任何类型,因为任何类型都实现了 0 个方法。
        空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
         
        类型断言 Type assertions
        提供了访问接口值底层具体值的方式。
        If i does not hold a T the statement will trigger a panic.
        If i holds a T, then t will be the underlying value and ok will be true.
        If not, ok will be false and t will be the zero value of type T, and no panic occurs.
         
        类型选择 Type switches 可实现泛型
        可实现泛型的效果,语法类似 switch 和 type assertion 的结合,把 type assertion 中的 i.(T) 换成了 i.(type) ,然后得到的结果跟 case 比较
         
        Stringer 接口
        Stringer 接口是 fmt 包中最常见的接口,实现之后用来通过字符串描述自身,也就是在 fmt.Print 打印时调用的
         
        错误 Error
        Go 程序使用 error 值来表示错误状态。与 fmt.Stringer 类似,error 类型是一个内建接口:
        在调用函数后,验证 err 是否是 nil,来判断是否出错
         

        泛型

        type parameters 泛型函数
        通过在函数定义中指定一个泛型 T,T 是一个满足约束 comparable 的类型,然后在函数参数中声明接收该类型。
        comparable 约束是指那些能够使用 == 和 != 做比较操作的类型。
        一般来说定义泛型函数都是在函数名和参数中间,加一个 [],里面定义类型 T 需要满足什么约束条件
         
        generic types 泛型类型
        在 type 中定义泛型
         

        goroutines

        go 开启协程
        go f(x, y, z) 启动一个协程并执行 f 函数,协程们都是运行在相同地址空间内,所以访问共享内容需要做同步控制,sync 包提供了同步原语
         
        channel 通道
        管道有类型,只能存取指定类型的数据。操作符 <- ,箭头就是数据流动的方向
        channel的创建需要用 make
        如果没指定capacity,则通道只能在发送和接收方都准备好时才可通信,啥意思呢?
        双向通道和单向通道
        单向通道直接声明出来没有意义,因为一直只能读不能写的通道意味着无法写入也就无法读出,反之也是一样。
        单向通道更多的是在特定场景下限制对双向通道的使用约束,例如将一个双向通道作为参数传入某函数中,该函数参数要求接收一个读出通道,意味着在本函数中,通道只能用作读出操作。同样的,另一个函数可以限制通道只能做写入操作。
        从一个空通道读取时出现 deadlock 问题
        下面代码运行会报错
        原因是从一个空的通道中读取会发生阻塞,也就是陷入 asleep,但代码中也再无其他协程会写入通道,实际上导致读出操作永远无法被唤醒,go 认为这是发生了死锁,所以抛出异常。
         
         
        channel 缓冲大小
        channel 可以指定缓冲大小,缓冲满了则写入会阻塞,缓冲空则读取会阻塞。
         
        range 遍历 channel 和 close 关闭 channel
        close(ch) 发送端可以关闭一个 channel,表示不再产生新数据了,但还可以从中取出数据,如果有的话,且最后一个通道里的数据被接受完之后,再次读取 <-chan 则不会阻塞,而是立即返回 0 和一个值为 false 的标志,表示通道已经关闭。
        在接收端可以通过 v, ok := <-ch 的 ok 值得知通道被关闭,如果被关闭,ok 值会是 false
        for i := range c 则可以遍历 channel 直到通道被关闭为止
         
        select 等待多个 channel
        有任何一个可以继续执行,就会执行该分之,当多个分之都准备好时,就随机选一个执行
         
        使用 default 指定默认执行的 select
         
        互斥锁 sync.Mutex
        sync.Mutex 提供两个方法
        • Lock
        • Unlock
        SafeCounter 结构体通过 Inc 方法,安全的增加 v Field,在增加v的前后会加上互斥锁和解除互斥锁

        © 菜皮 2020 - 2023