Go语言基础学习

发布时间 2023-08-22 00:14:23作者: Geraltz'Rivia

一些链接

go语言规范

go命令

go的坑

GMP调度

基本语法

go教程

循环语句

for init; condition; post {

}

不需要 括号 ,但是需要花括号包住循环体, init 与 post 语句可以忽略,分号也可以忽略,所以java中的while condi在go中就是 for cond,

可以直接 for {} 无限循环

go中可以用 lable 与 goto 语句控制程序的执行流,除了常规的 break , continue语句控制循环之外,还可以在 break与continue后面加上label表示要执行哪个循环体

   fmt.Println("---- break label ----")
   re:
      for i := 1; i <= 3; i++ {
         fmt.Printf("i: %d\n", i)
         for i2 := 11; i2 <= 13; i2++ {
         fmt.Printf("i2: %d\n", i2)
         break re
      }
   }

也可以使用break 跳出 switch

条件语句

if stat; condition {

} else {

}

if 与 for 类似,省略括号,但是必须包含花括号,if 条件之前可以执行一句statment,变量可以在if 范围生效

switch 语句

switch var {

case var1:

case var2:

default:

}

go的switch各个条件默认加了break,而且case也可以是变量,可以switch var.(type)判断变量的类型

使用fallthrough关键字可以强制执行下一个case中的语句

case true就默认执行;

default语句无论放到什么位置都是最后判断

如果case表达式中子表达式的结果值是无类型的常量,那么它的类型会被自动地转换为switch表达式的结果类型,如果不能转化那么编译报错

switch会从上往下求值,如果使用无类型的常量当作case,不允许使用同样的常量,但是可以通过 case 变量来绕过,这时候如果有符合switch条件的case同时发生,就会选择上面的语句执行

go 函数

func function_name( [parameter list] ) [return_types] { 函数体 }

函数有两种传参方式:值传递与引用传递

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。引用传递可以像c语言一样传 & 地址

结构体的方法

func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ },然后可以使用variable_name.function_name()调用

值方法与指针方法

那么值方法和指针方法之间有什么不同点呢?它们的不同如下所示。

  1. 值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。
  2. 一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法。严格来讲,我们在这样的基本类型的值上只能调用到它的值方法。但是,Go语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。比如,在Cat类型的变量cat之上,之所以我们可以通过cat.SetName("monster")修改猫的名字,是因为Go语言把它自动转译为了(&cat).SetName("monster"),即:先取cat的指针值,然后在该指针值上调用SetName方法。
  3. 在后边你会了解到,一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。

defer

defer语句就是defer关键字后跟一个函数调用语句。defer关键字的作用在于对关键字之后的函数调用语句进行延迟执行。当执行到defer语句时,函数和参数表达式得到计算,但是直到该语句所在的函数执行完毕时,defer语句才会执行

所谓的函数执行完毕包含return结束或者是因为panic结束

函数中可以有多个defer语句,函数返回时,defer语句执行的顺序与生命顺序相反

当执行到defer语句时,函数和参数表达式得到计算

数组 切片

  • 数组

var variable_name [SIZE] variable_type

e.g. var balance [10] float32

初始化:

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 元素数量未知

balance := [5]float32{1:2.0,3:7.0} // 将索引为 1 和 3 的元素初始化

多维数组 var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

  • 切片

Go 语言切片是对数组的抽象。Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

初始化 var slice1 []type = make([]type, len) 也可以简写为 slice1 := make([]type, len)

也可以指定容量,其中 capacity 为可选参数 make([]T, length, capacity)

初始化切片 s,是数组 arr 的引用。s := arr[startIndex:endIndex],由于是引用,所以对切片的修改也会影响到原数组与其他切片

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

func append(s []T, vs ...T) []T 可以添加元素,copy(s1, s2) 把s2的切片拷贝到s1

指针

var a int= 20 /* 声明实际变量 */var ip int / 声明指针变量 / ip = &a / 指针变量的存储地址 */

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

结构体

type struct_variable_type struct { member definition member definition ... member definition }

初始化

variable_name := structure_variable_type {value1, value2...valuen}

variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

结构体与结构体的指针都可以通过 . 来访问成员变量

空结构体的使用

实现方法接收者。

实现集合类型。只取key,空value

实现空通道。

https://segmentfault.com/a/1190000040799205

range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对

for key, value := range oldMap { newMap[key] = value }

for _, value := range oldMap

for index, ele := range array/slice,如果只用一个变量接受那么就只获得了索引

在使用range遍历数组或切片时,数组是值类型,计算range的时候是复制一份;所以element的结果都是原数组中的值;

如果是切片,切片是引用类型,ele会随着遍历操作发生变化,比如前一个循环体中修改了后续slice中的元素值,就会导致后续拿到的值发生变化

map

Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。

创建map

map_variable := make(map[KeyType]ValueType, initialCapacity)

插入或更新

m[key] = elem

获取元素

elem = m[key]

删除元素

delete(m, key)

测试key是否存在

elem, ok = m[key]

If key is in m, ok is true. If not, ok is false.

If key is not in the map, then elem is the zero value for the map's element type.

Go 语言接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。

Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。

/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 /
type struct_name struct {
/
variables */
}

/* 实现接口方法 /
func (struct_name_variable struct_name) method_name1() [return_type] {
/
方法实现 /
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/
方法实现*/
}

空接口 interface{}

https://juejin.cn/post/6844904183770906638

错误处理

go error handling

Go语言的错误处理机制是通过返回一个error类型的值来实现的。在Go语言中,如果一个函数可能会出现错误,它通常会返回一个error类型的值,表示函数执行过程中可能出现的错误。如果函数执行成功,它会返回nil,表示没有错误发生。

if err := MyFunc(); err != nil { // 处理错误 }

在Go语言中,error类型是一个接口类型,它定义了一个Error()方法,用于返回错误的字符串描述。因此,自定义的错误类型只需要实现Error()方法即可

type MyError struct {

message string

}

func (e *MyError) Error() string {

return e.message

}

范型

Go语言的泛型参数(type parameter)是在Go 1.18版本中引入的一项新特性。它允许在函数、方法、结构体和接口等代码块中定义类型参数,从而使代码更加通用和灵活,可以处理不同类型的数据,而不需要为每种类型都写一个专门的函数或类型。

在Go语言中,泛型参数使用方括号([])来表示,例如:

func MyFunc[T any](x []T) { // ... }

这个函数定义了一个类型参数T,它可以代表任意类型。在函数体中,可以使用T类型的变量和常量,以及T类型的运算符和函数。在调用这个函数时,需要指定T的具体类型,例如:

MyFunc[int]([]int{1, 2, 3})

这里指定了T为int类型,传递了一个int类型的切片作为参数。

泛型参数还支持约束(constraint),可以限制T类型必须满足某些条件,例如:

func MyFunc[T comparable](x []T) { // ... }

这个函数定义了一个类型参数T,它必须满足comparable的约束,也就是说,T类型必须支持==和!=运算符。这样可以避免在函数中使用不支持比较运算的类型,从而提高代码的安全性和可靠性。

Go语言的泛型参数是一项非常强大的特性,它可以使代码更加通用和灵活,避免重复的代码,提高代码的可读性和可维护性。(generated by chatGPT)

itoa常量

https://segmentfault.com/a/1190000000656284

类似于java中的enum

自增长常量经常包含一个自定义类型,允许你依靠编译器。

type Stereotype int

const (

TypicalNoob Stereotype = iota // 0

TypicalHipster // 1

TypicalUnixWizard // 2

TypicalStartupFounder // 3

)

用下划线 _ 能略过一些值

init()方法

https://zhuanlan.zhihu.com/p/34211611

导入包

https://www.jianshu.com/p/ed54ac167d58

判断类型

https://golang.google.cn/ref/spec#Type_assertions

value, ok := interface{}(container).([]string)

判断container变量是否是 []string 类型的,类型判断断言 x.(T) x是需要判断的,必须是接口类型,T是目标类型

类别名

type关键字

type mystring = string // mystring 与 string 是同一类型

type mystring string // 两者是不同类型 (类型再定义)

并发编程模型

goroutine

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的

go f(x, y, z)

channel

ch <- v // Send v to channel ch.

v := <-ch // Receive from ch, and // assign value to v.

// create a channel

ch := make(chan int)

默认这种形式的channel会阻塞,使用带缓冲区的channel

ch := make(chan int, 100)

发送到满的channel会阻塞,接受一个空的channel会阻塞。

v, ok := <-ch 判断channel是否关闭,ok是false的时候代表channel已经关闭了,应该由sender执行这一操作

channel并不一定需要关闭,一般在接受者循环等待的时候才需要关

close(ch)

  • 通道可以使用range遍历,而且就算通道被关闭了,通道中的值不会被抛弃,能被读取出来,参考 https://gobyexample-cn.github.io/range-over-channels
  • 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
  • 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
  • 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
  • 写已经关闭的 chan 会 panic
  • 读写一个未初始化的channel都会阻塞

select 语句

select可以监听一系列通道上的操作,有一个通道就绪就执行其中的代码,多个通道就绪随机执行

使用default可以不阻塞执行,如果没有通道准备好就执行default中的内容

Mutex

sync.Mutex and its two methods:

  • Lock
  • Unlock

可以使用defer关键字确保解锁

defer c.mu.Unlock()

return ...

条件变量 sync.Cond

获取锁;

while (条件状态不满足) {

释放锁;

线程挂起等待,直到条件满足通知;

重新获取锁;

}

临界区操作;

释放锁;

sync.Pool

一篇非常完善的源码级分析https://www.cnblogs.com/qcrao-2018/p/12736031.html

标准库

https://syaning.github.io/go-pkgs

container

list

双向链表

  • Element 结构体:链表中的元素,包含了指向下一个元素和上一个元素的指针,以及元素所在的链表和元素的值。
  • List 结构体:链表,包含了一个哨兵元素 root 和链表的长度 len。哨兵元素 root 是一个空元素,它的 next 指针指向链表的第一个元素,prev 指针指向链表的最后一个元素,这样可以方便地进行链表的操作。
  • 链表的操作方法:包括初始化链表、获取链表长度、获取链表的第一个元素和最后一个元素、在链表的前面或后面插入元素、在指定元素前后插入元素、移动元素到链表的前面或后面、在指定元素前后移动元素、删除元素等。

以下是对一些关键函数的解释:

  • Init():初始化或清空链表,将 root 元素的 next 和 prev 指针都指向自己,并将链表长度设为0。
  • insert(e, at *Element):在 at 元素后面插入元素 e,并增加链表长度。
  • remove(e *Element):删除元素 e,并减少链表长度。
  • move(e, at *Element):将元素 e 移动到 at 元素后面。
  • PushFront(v any):在链表前面插入一个值为 v 的新元素。
  • PushBack(v any):在链表后面插入一个值为 v 的新元素。
  • InsertBefore(v any, mark *Element):在 mark 元素前面插入一个值为 v 的新元素。
  • InsertAfter(v any, mark *Element):在 mark 元素后面插入一个值为 v 的新元素。
  • MoveToFront(e *Element):将元素 e 移动到链表的前面。
  • MoveToBack(e *Element):将元素 e 移动到链表的后面。
  • MoveBefore(e, mark *Element):将元素 e 移动到 mark 元素的前面。
  • MoveAfter(e, mark *Element):将元素 e 移动到 mark 元素的后面。
  • PushBackList(other *List):将另一个链表 other 的元素复制到当前链表的后面。
  • PushFrontList(other *List):将另一个链表 other 的元素复制到当前链表

ring

  • type Ring struct:定义了环形链表的结构体,包含 next、prev 指针和一个 Value 字段,用于存储客户端的数据。
  • func (r *Ring) init() *Ring:初始化环形链表,将 next 和 prev 指针指向自身。
  • func (r *Ring) Next() *Ring:返回环形链表的下一个元素。
  • func (r *Ring) Prev() *Ring:返回环形链表的前一个元素。
  • func (r *Ring) Move(n int) *Ring:将环形链表向前或向后移动 n 个元素。
  • func New(n int) *Ring:创建一个包含 n 个元素的环形链表。
  • func (r *Ring) Link(s *Ring) *Ring:将环形链表 r 和 s 连接在一起。如果 r 和 s 是同一个环,连接它们会从环中移除 r 和 s 之间的元素。如果 r 和 s 是不同的环,连接它们会将 s 的元素插入到 r 之后。
  • func (r *Ring) Unlink(n int) *Ring:从环形链表 r 中移除 n 个元素,返回被移除的子环。
  • func (r *Ring) Len() int:计算环形链表 r 的元素个数。
  • func (r *Ring) Do(f func(any)):遍历环形链表,对每个元素调用函数 f

heap

Go 语言实现的堆操作库,用于实现任何实现了 heap.Interface 的类型的堆操作。堆是一种树形数据结构,具有每个节点都是其子树中最小值节点的特性。堆通常用于实现优先队列。

  1. 代码首先定义了一个名为 heap 的包,并引入了 sort 包。
  2. 定义了一个名为 Interface 的接口,它描述了使用此包中的函数所需的类型要求。实现此接口的任何类型都可以作为最小堆使用,其中包含了 sort.Interface、Push(x any) 和 Pop() any 三个方法。
  3. Init(h Interface) 函数用于初始化堆,确保堆满足其他函数所需的堆不变式。时间复杂度为 O(n),其中 n 为堆的元素数量。
  4. Push(h Interface, x any) 函数用于将元素 x 压入堆中。时间复杂度为 O(log n),其中 n 为堆的元素数量。
  5. Pop(h Interface) any 函数用于移除并返回堆中的最小元素(根据 Less 方法定义)。时间复杂度为 O(log n),其中 n 为堆的元素数量。Pop 等同于 Remove(h, 0)。
  6. Remove(h Interface, i int) any 函数用于移除并返回堆中索引为 i 的元素。时间复杂度为 O(log n),其中 n 为堆的元素数量。
  7. Fix(h Interface, i int) 函数用于在索引为 i 的元素值发生变化后重新建立堆顺序。在索引 i 的元素值发生变化后调用 Fix 等同于调用 Remove(h, i),然后将新值压入堆中,但成本更低。时间复杂度为 O(log n),其中 n 为堆的元素数量。
  8. up(h Interface, j int) 和 down(h Interface, i0, n int) 是两个辅助函数,分别用于向上和向下调整堆中元素的位置,以满足堆的性质。

排序

sort.Strings()

sort.Ints()

排序会直接改变原切片,对所有的内置类型都有排序的方法

自定义排序

为了在 Go 中使用自定义函数进行排序,我们需要一个对应的类型。 我们在这里创建了一个 byLength 类型,它只是内建类型 []string 的别名。

我们为该类型实现了 sort.Interface 接口的 Len、Less 和 Swap 方法, 这样我们就可以使用 sort 包的通用 Sort 方法了, Len 和 Swap 在各个类型中的实现都差不多, Less 将控制实际的自定义排序逻辑。 在这个的例子中,我们想按字符串长度递增的顺序来排序, 所以这里使用了 len(s[i]) 和 len(s[j]) 来实现 Less。

我们就可以通过将切片 fruits 强转为 byLength 类型的切片, 然后对该切片使用 sort.Sort 来实现自定义排序。

package main

import (
    "fmt"
    "sort"
)

type byLength []string

func (s byLength) Len() int {
    return len(s)
}
func (s byLength) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
func (s byLength) Less(i, j int) bool {
    return len(s[i]) < len(s[j])
}

func main() {
    fruits := []string{"peach", "banana", "kiwi"}
    sort.Sort(byLength(fruits))
    fmt.Println(fruits)
}

Unix信号处理

signal.Notify 注册给定的通道,用于接收特定信号。signal.Notify(sigs, syscall.SIGINT ...)

java中也有相似的应用,可以实现接口 SignalHandler 的 handle 方法,处理系统的信号

语言规范

文件名使用小写加下划线

包名使用小写

变量名使用驼峰式

结构体使用驼峰式,根据可不可导出确定首字母大小写

接口命名规则与结构体相同

变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

如果变量为私有,且特有名词为首个单词,则使用小写,如 appService

若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头

常量均需使用全部大写字母组成,并使用下划线分词

如果是枚举类型的常量,需要先创建相应类型

单元测试文件名命名规范为 example_test.go 测试用例的函数名称必须以 Test 开头,例如:TestExample 每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试

错误处理不要使用 _ 接受err,一定要处理