Go For Grammar

czc

Hello world

1
2
3
4
5
6
7
8
9
10
packge main
// 包声明 声明此文件属于哪个包

import "fmt"
// 引入需要调用的包

func main(){
// 输出
fmt.PrintLn("Hello world")
}

数据类型

声明 函数 && 变量

目的:充分利用内存 → 需要大数据的时候申请大内存 需要小的时候申请小内存

类型 详解
布尔 true false
数字 int float
字符串 使用UTF-8标识Unicode文本
其他类型 指针 数组 结构化 Channel

以下为基本类型

类型 描述
uint8 / uint16 / uint32 / uint64 无符号 8 / 16 / 32 / 64 位整型
int8 / int16 / int32 / int64 有符号 8 / 16 / 32 / 64 位整型
float32 / float64 IEEE-754 32 / 64 位浮点型数
complex64 / complex128 32 / 64 位实数和虚数
byte 类似 uint8
rune 类似 int32
uintptr 无符号整型,用于存放一个指针

[^ps]:有关多态类型转换 类型断言见 接口章节中的 转换类型-赋值 类型断言

关于goroutine

goroutine 是 Go 语言中用于实现并发编程的一种轻量级线程。

  1. 轻量级: goroutine 是轻量级的执行单元,由 Go 运行时系统调度。与传统线程相比,goroutine 的创建和销毁开销很小。
  2. 并发性: Go 语言的并发模型建立在 goroutine 的基础上。可以通过简单的关键字 go 来启动一个新的 goroutine,例如:go myFunction()
  3. 通信通过通道: 在 goroutine 之间进行通信通常使用通道(channel)。通道是一种类型,用于在 goroutine 之间传递数据。这有助于避免共享内存的复杂性和竞争条件。
  4. Go 调度器: Go 运行时系统有一个调度器,负责在可用的逻辑处理器(logical processor)上调度 goroutine 的执行。这个调度器会在 goroutine 阻塞时将处理器分配给其他可运行的 goroutine。
  5. 垃圾回收: Go 语言自带垃圾回收机制,用于管理内存。这减轻了开发者的负担,不需要手动管理内存。

条件控制 && 分支语句

与c java中大同小异 直接上语法

  • if语句
1
2
3
if xxxx{
// xx处为布尔表达式
}
  • if-else
1
2
3
if xxxx{
} else {
}
  • switch
1
2
3
4
5
6
7
8
9
10
11
switch a{
case val1:
...
case val2:
...
fallthrough
case val3:
...
default:
...
}

基本用法同其他语言 但是每一个switch分支执行了就自动break

多了一个fallthrough关键字 就是贯通后续的case 相当于取消这个break

  • select
1
2
3
4
5
6
7
8
select{
case msg1:
...
case msg2:
...
default:
...
}

此处仅给出大致骨架 用法说明及例子见channel章节中select语句部分内容

循环及其控制语句

大致与java c类似

  • 经典老三样
1
2
3
4
5
6
7
for (init);(条件表达式);(循环后操作语句){
// 循环体
}
//eg
for i := 0;i < 5; i ++{
fmt.Println(i)
}
  • 类似while (老一样)
1
2
3
4
5
6
7
8
for (条件表达式){
}
// eg
count := 0
for count < 5{
fmt.Println(count)
count ++
}
  • 无限循环(类似for(;;)
1
2
3
for{
// 无限循环体
}

控制语句

  • break

与c java使用方式相同 作用相同 不再赘述

  • continue

与java c的使用方式基本相同

并且在多重循环中可以结合label标出想要跳出的循环

有一点类似c中的goto 这个还是不建议使用吧

实例:

1
2
3
4
5
6
7
8
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}

直接continue到外层循环中

  • goto

大名鼎鼎的goto 跟c中用法相同 用法上就是汇编中的cmp 无条件转移

一般不让用

1
2
3
4
5
6
7
8
9
a := 0
LOOP: for a < 5 {
if a == 2 {
a ++
goto LOOP
}
fmt.Printf("%d\n", a)
a++
}

基本类型变量及其特点

单变量声明

字母 数字 下划线 组成 首字符不能为数字

一般定义格式:

1
2
3
var identifier typename
// 如下
var a int = 1

在java中 如果未对基本类型初始化 jvm会自动赋为对应类型的初始值

go中有类似机制

类型
布尔 0
数值 false
字符串 “”

也可以将如上代码利用 := 简写 格式如下:

1
2
3
4
5
6
7
typeName := value
// 如下
intVal := 1

// 将以上写法由一般定义格式写回来就是
var intVal int
intVal = 1

另外 在go中可由以下方式 通过变量的初始值来判断变量类型 (由编译器决定他的数据类型)

1
2
3
4
5
func main(){
var d = true
// 此时d的类型为布尔型
fmt.Println(d)
}

多变量声明

同时声明多个类型相同的变量(非全局) 如下:

Q:使用这种方法定义变量时 需要保证他前后的数据类型是一样的吗?

A:变量的数据类型会被根据赋值的表达式自动推导。在给多个变量赋值时,确保它们的数据类型是一样的是一种最佳实践,但并不是强制要求。

1
2
3
var x,y int
var c,d int = 1, 2
g, h := 123, "hello"

同理可对全局变量做多声明 如下

1
2
3
4
5
6
7
8
9
var (
vname1 v_type1
vname2 v_type2
)
// eg
var (
a int
b bool
)

匿名变量

同java中 匿名 的意义 不能在后续代码中使用 赋值 运算

标识方式为下划线 _. (又被成为 空白标识符)

可以被赋值为任何一个类型的元素

可理解成一个叫 _ 的特殊变量 同一单元内只可以被一次性使用

1
2
3
4
5
6
7
8
9
10
11
12
func retValue(int , int){
return 1, 2
}
func main(){
a,_ := retValue()
// 上方下划线赋值为20
_,b := retValue()
// 上方下划线赋值为10
// 但由于匿名变量特性无法调用他们的值

// (:= 符号见上方)
}

特点:不占用空间 分配内存

作用域

变量的作用范围

变量名 定义上下文位置 作用域 注意点
局部变量 函数内定义 函数体内 存在于对应函数调用开始 销毁于调用结束
全局变量 函数外定义 所有源文件(外部包) var关键字开头 外部引用的需要首字母大写

指针

定义

类似 c/c++ 中的指针 取地址符为 &

下方表格为两者差异

特征 C/C++ Go
内存管理 手动分配和释放内存 自动垃圾回收
指针运算 允许指针算术运算 限制指针算术运算
空指针 NULLnullptr表示空指针 nil表示空指针
安全性 较低,易出现内存泄漏和悬空指针等问题 相对较高,减少了一些常见的内存错误
指针传递 经常用于传递引用或修改外部变量 用于传递引用,但有更多控制和安全性

声明 & 初始化

声明类似及指针变量名字

1
2
3
4
5
var var_name *var-type  
// 同c *号用于指明对应变量为指针类型
//eg
var intPoint *int
var floatPoint *float64

初始化指针变量

1
2
3
4
5
6
var a int = 1	// 实际变量初始化
var intPoint *int // 声明指针变量
ip = &a // 取实际变量的地址 给指针变量赋值

//同理 也可以利用 := 运算符简化代码为
ip := &a

空指针

老生常谈的东西 此处指指针变量的值为 即没有分配到任何变量的地址 写做 nil


数组

定义同java c其他语言大致相同

声明

1
2
3
4
var variable_name [SIZE] variaaable_type
// eg
var num [10] int
// 声明整型长度为10的数组

初始化数组

可依照初始化的 范围 已知条件分类

  • 直接初始化

    1
    var num = [5]int{1,2,3,4,5}
  • 短变量声明初始化

    1
    num := [5]int{1,2,3,4,5}
  • 长度不确定 编译器通过元素个数自行推断数组长度

    1
    2
    var num = [...]int{1,2,3,4,5}
    num := [...]int{1,2,3,4,5}

    !!!使用这种方式需要确定数组内的所有元素(已知目标数组内的元素)

  • 已知长度 部分初始化指定下标内容

    1
    2
    3
    num := [5]int(1:0, 3:1)
    // 初始化下标为1(第2项值为0)
    // 初始化下标为3(第4项值为1)

[^注意]: 若忽略 [] 中的数字不设置大小 Go会根据元素个数设置数组的大小

指针相关

1
2
var a = [...]int{1,2,3,4,5}
var b = &a

注意!在c语言中如上的写法 一般会理解为将a数组的首地址传给b

但是在形如 java go 的高级语言中 是将整个数组变量传过去的 以上代码块为例 b是一个代表了整个a数组的指针变量

并且在开发中经常会遇到数组变量复制 传递的需求 此时将整个数组复制的时空开销是很大的 例如java中的 System.arrayCopy()等方法

此时可以利用指针特性 直接传递数组指针


结构体

类比c语言中的 struct java中的 javabean

定义 & 声明变量 todo

1
2
3
4
5
6
7
8
9
10
type struct_variable_type struct {
member definition
member definition
...
}
// eg
type Person struct {
Name string
Age int
}
1
2
3
4
5
variable_name := structure_variable_type{value1,value2}
// 部分初始化 这里的key1 key2是具体结构体中属性的名字
variable_name := structure_variable_type{key1:value1,key2:value}
// eg
person := Person{Name: "Alice", Age: 30}

访问结构体属性

1
2
3
4
5
6
7
variable_name.key = value
// eg
func update(){
var person Person
person.Name = "Alice"
person.Age = 30
}

此处 也许读者会发问 go中不需要采用get/set方法 而是直接修改对应对象中属性的值吗?这样会导致不合法的修改和访问吗?

在实际开发中,Go 社区倾向于更加简洁和直接的方式。如果字段的含义清晰明了,并且不需要额外的逻辑控制或边界检查,直接访问和修改公开字段是一种常见做法。

在 Go 中,许多设计决策都是为了简化语言、提高开发效率和清晰度。Go 的设计哲学之一是”不要引入不必要的复杂性”。这可能会导致某些情况下牺牲了一些严格的安全性,但同时也增加了开发的灵活性和速度。

指针相关

定义 声明
1
2
3
var struct_pointer *structure_variable_type
// eg
var personPointer *Person

若需要通过指针访问结构体成员

1
2
3
struct_pointer.title
// eg
personPointer.Name
用法
  • 效率传递

与上文数组指针同理 不再赘述

  • 修改原始数据

指针直接修改对应地址原始值 不需要返回修改后的结构体 (传递址修改值

  • 堆内存动态分配

Q: 何谓堆与动态分配?

A: 堆内存动态分配是指在程序运行时根据需要分配内存。通过 new()make() 等方式可以在堆上动态创建数据,并且该数据在还有应用时将一直存在

注意!并不是程序结束前 也并不是被调用函数结束前 其原理是 一旦没有了指向这个实例的指针 它将被垃圾回收机制回收

此方法有别于一般的栈上分配的局部变量

如果你在函数内部声明一个结构体变量并将其指针传递给其他函数,这个结构体变量是在栈上分配的。它的生命周期受限于所在函数的执行周期一旦函数执行结束,栈上分配的变量将被销毁。

1
2
3
4
func createPerson() *Person {
p := new(Person) // 创建结构体指针
return p
}

这种方法可以是实现多个函数间的实例对象共享 调用 修改


字符串

定义

不可改变的字节序列 是一个 只读 的字节数组

底层结构如下 (位于 reflect.StringHeader 包中)

1
2
3
4
5
6
type StringHeader struct{
Data uintptr
// 字符串数据 uintptr为数组指针
Len int
// 字符串长度
}

关于uintptr

uintptr 是一个不安全的整数类型,可以进行指针运算而不受到 Go 语言的内存安全检查的限制。

在 Go 语言中,直接进行指针运算是不被鼓励的,因为 Go 的内存管理是自动的,而指针运算可能会导致未定义行为。然而,在一些底层的系统编程或涉及到 C 语言交互的情况下,可能需要使用 uintptr 进行指针运算。

也就是说 在这种情况下uintptr是作为底层数组指针的形式出现

本质上就是一个包装后的字节数组结构体 此处不再赘述

编码

默认采用 UTF-8 编码 若编码错误 将一般会显示此字符

类型转换 (to do)

切片*

定义

可以变长的动态数组

类比java中的arrayList 其实现的功能以及特点都相似 此处不再赘述

在一般的开发中 由于其变长的特点 适配于大多数的开发当中 数组相对没这么广泛

操作要点:保证append(尾增加) 等操作不会超出容量 降低内存扩展的次数 防止空间浪费 根据需求合理定义最大长度

底层结构如下 reflect.SliceHeader

1
2
3
4
5
6
7
8
type SliceHeader struct{
Data uintptr
// 底层数组指针
Len int
// 目前长度
Cap int
// 最大容量
}

为什么uintptr却是指针?

详见字符串中解释 (以址传递)

一般情况下 切片的正常数据指针和长度信息是对应的 如果数据指针为空 长度容量不为空 则说明切片本身损坏

在容量不扩展的前提下 对切片的操作和对数组的操作基本相同

切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型;数组的类型是由元素的类型和数组的长度共同决定的。如何理解?

在 Go 中,数组的类型是由元素的类型和数组的长度共同决定的,而长度是数组类型的一部分。

[5]int[10]int 是两种不同的数组类型,因为它们的长度不同。[5]int 表示一个包含5个整数的数组,而 [10]int 表示一个包含10个整数的数组。这两者的元素类型都是 int,但由于长度不同,它们是不同的数组类型

而当你使用 [] 来定义一个变量时,它默认是一个切片。在定义切片时,你无需提供长度信息,因为切片是动态长度的数据结构,长度可以根据需要动态改变。只要是相同类型元素构成的切片均对应相同的切片类型

基本操作

append() 拼接函数

添加元素
  • 尾部添加
1
2
3
4
5
6
7
var a []int
a = append(a,1)
// 尾部添加单元素
a = append(a,1,2,3)
// 尾部添加多元素
a = append(a,[]int{1,2,3})
// 尾部添加另外切片
  • 头部添加
1
2
3
var a = []int{1,2,3}
a = append([]int{0},a...)
a = append([]int{-3,-2,-1}, a...)

在go底层中 会在切片的头部预留空位方便头插

但是一旦超过 就会导致内存重新分配 并且元素全部重新赋值一次 比尾插效率更低

尽量可以使用双端队列或其他链式结构来优化

  • 链式操作
1
2
3
var a []int
a = append(a[:i],append([]int{1},a[i:]...)...)
// 第i位置插入a

1.append([]int{1},a[i:]...)

创建匿名切片 []int{1},放入切片a[i:]后方 代表将a切片中第i个位置后的元素都放到1后方

关于...符号

此处的...是起打散参数的作用 可知代码中将整个切片作为参数插入 利用此符号将切片中的每一项都作为一个实参 作为可变数量的参数传递给函数

说白了就是 可变参数的使用方式

  1. 外部append

将第一步中得到的切片作为实参,再与原来的a[i:]切片进行连接

  • append和copy组合
1
2
3
a = append(a,0)
copy(a[i + 1:],a[i:])
a[i] = x

同样是起到append的作用 但是这种方法不需要临时的切片对象 是一种空间上的优化

删除元素
  • 开头位置删除 头删

移动数据(底层数组)指针 原地删除

1
2
3
a = []int{1,2,3, ...}
a = a[1:]
a = a[N:]

这种方法本质上就是从头筛选切片中自己所想要的元素 复制回给自己 实际上是对底层数组进行共享而不会拆功能键新的数组

或可使用append完成 也为原地删除

1
2
3
4
5
a = []int{1,2,3}
a = append(a[:0], a[1:]...)
// 删除开头1个元素
a = append(a[:0], a[n:]...)
// 删除开头n个元素

注意 所谓a[:0]指的是从索引为0开始 但不包含0的索引 等价于a[0:0] 即为空切片

所以上面的操作实际上没什么不一样 只是将删减后的切片和一个空切片合并而已 因为append第一个地方不能nil 你要随便声明一个切片也不是不行

这么看的话使用append好像显得有点多余 但是如果是在中间位置对这个切片动手就完全不一样了

  • 中间位置

append:

1
2
3
4
a = append(a[:i], a[i+1:]...)
// 删除i后的一个元素
a = append(a[:i], a[i+n:]...)
// 删除i后的n个元素
  • 尾部删除
1
2
3
4
5
6
a = a[]int{1,2,3, ...}

a = a[:len(a) - 1]
// 删除尾部1个元素
a = a[:len(a) - n]
// 删除尾部n个元素

不必说 删除尾部效率最高 (不需要挪位置不需要复制)

函数

同java方法 c函数概念

可见级别

go中方法与变量相同,可见性都分为两个级别

包内可见性 包外可见性
private public
首字母小写(test) 首字母大写(Test)
只能在对应包内使用 可在其他包中引入使用

以包为分界线划分 可见性是以包为单位基于包的

函数分类

类别 描述 例子
全局函数 在包级别声明的函数,整个包可访问 func GlobalFunction() {}
局部函数 在其他函数内部声明的函数,仅在该函数内可见 func outerFunction() { localFunction := func() {} }
方法 与结构体关联的函数,类似于面向对象语言中的方法 type MyStruct struct {} func (m MyStruct) Method() {}
匿名函数 没有名字的函数,可以直接赋值给变量或作为参数传递 add := func(a, b int) int { return a + b }
闭包 包含自由变量的函数值,可以访问封闭范围内的变量 func closureExample() func() int { sum := 0; return func() int { sum++; return sum } }

定义格式

1
2
3
func fuction_name([parameter list])[return types]{
// 函数体
}

(不再赘述)无参或返回值为空 则不写

返回值

与java c不同 go中可以定义 一个或多个返回值 并且可以定义返回值的名字

1
2
3
4
func Find(m map[int]int, key int)(value int, ok bool) {
value,ok = m[key]
return
}

在如上程序段中 进行如下操作

  • 在声明处 声明了value ok为返回值

    [^ 补充]: 一般称这种函数定义时指定名称的返回值为 命名返回值 其与非命名返回值具有一定的差异 eg下方defer闭包

  • 程序段中赋值

  • return

正常的情况下 我们可以对它进行调用并且正常直接赋值到变量中

1
2
3
4
func getData(){
myMap := map[int]int{1: 100, 2: 200, 3: 300}
result, found := Find(myMap, 2)
}

defer关键字*

用于延迟(defer)函数的执行直到包含它的函数结束之前。无论函数是正常返回还是发生了错误,defer都会确保延迟的函数会被执行。

执行时机为return语句前 其他语句之后

所以其一般在 流的释放 异常处理等具有很关键作用

当同一函数出现多个defer怎么办?

多defer运行遵循LIFO原则 则栈可将其类比成 过滤器链FilterChain 的处理方式

[^ 补充]:Go 编译器而是创建了一个延迟函数链表(deferred function list)来管理和执行 defer 语句中的延迟函数。 这个链表是一个数据结构,用于按照后进先出(LIFO)的顺序存储被 defer 注册的延迟函数。每次调用 defer 语句时,新的延迟函数节点会被插入到链表的头部,因此最后注册的 defer 会最先执行。这个链表通常是在栈上实现的,每个 Goroutine 都有自己的延迟函数链表。在函数返回之前,Go 会检查当前 Goroutine 的延迟函数链表,按照链表顺序执行其中的延迟函数。

关于多defer数据冲突

对于延迟函数的参数,在注册 defer 语句时就会被确定,而不是在实际执行延迟函数时再进行求值。这种设计确保了参数的值在 defer 语句执行时被捕获和固定,而不是在延迟函数执行时才求值。

人话:defer关键字中在入栈**(注册)的时候就已经确定 但此值可以是地址值也可以是变量值** 所以存在两种情况

  1. 注册变量值
1
2
3
4
5
6
7
func main(){
x := 10
defer func(a int){
fmt.Println(a)
}(x)
x ++
}

**补充:**上方内部func为匿名函数的一种用法的变体——在定义的时候立即执行

1
2
3
func() {
// 函数体
}()

格式如上 括号 () 就是表示立即执行的部分,而在括号内部可以传递参数。这样的匿名函数在定义时立即执行,通常用于创建一个局部的作用域,以便在其中执行一些特定的逻辑

一般用于 文件处理 计时器 错误处理

言归正传 上方保存的是入栈时x这一变量的值 所以最后输出的是 10

  1. 注册地址值

    1
    2
    3
    4
    5
    x := 10
    defer func(a *int){
    fmt.Println(*a)
    }(&x)
    x ++

此处入栈的是x的指针 输出的是对应x在变化之后的值 所以最后输出11 (变量值改变与地址无关

上方所说的都是一般正常使用需要注意的地方 但当defer结合其他就会有更多玩法 补充如下

版本1:

1
2
3
4
5
6
7
func test() (x int) {
x = 10
defer func() {
x++
}()
return x
}

版本2:

1
2
3
4
5
6
7
func test1() int {
x := 10
defer func() {
x++
}()
return x
}

两函数正确返回值分别为 11,10

观察两个函数的不同点 在于且仅在于前者的x为命名返回值

  • 若x为命名返回值 即x值在整个函数中是可见的 对于内部的匿名函数来说就变成了闭包函数 他所修改的是x本身的值

  • 若x非命名返回值 在内部函数的修改当中修改的是局部变量x的值 是一个独立于返回值的x的副本

从defer的执行时机上来说

  1. 计算返回值:return语句处,首先计算出要返回的值。对于未命名返回值的函数,这就是return语句后的那个值。
  2. 执行defer语句: 在函数实际返回之前,执行所有的defer语句。
  3. 返回: 函数完成返回操作,将步骤1中计算的返回值传递回调用方。

test1()这个函数中,虽然defer语句在函数返回之前执行,但是它修改的是局部变量x的值,而不是返回值。函数的返回值是在return x语句执行时确定的,这时x的值是10。即使defer语句随后将局部变量x的值增加到11,这个变化也不会影响已经确定的返回值。反之 如果是命名返回值,其值可以在返回值确定之后再次修改

底层角度有分析得

未命名返回值

  1. 计算return语句的值:编译器计算return语句后面的表达式的值,并将这个值存储在一个临时位置。
  2. 执行defer语句:接着,编译器执行任何defer语句。这些语句操作的是函数内的局部变量,而不是返回值。
  3. 返回操作:最后,编译器将存储在临时位置的值作为函数的返回值。

在这个过程中,defer语句是在计算返回值之后执行的,因此它不能改变已经确定的返回值。

命名返回值

  1. 命名返回值作为局部变量:在函数开始时,编译器将命名的返回值当作函数的一个局部变量来处理。
  2. 赋值给返回变量:在return语句执行时,编译器将值赋给这个局部变量(命名返回值),而不是存储在临时位置。
  3. 执行defer语句defer语句随后执行,可以访问并修改这个局部变量。
  4. 返回操作:最后,函数返回这个局部变量的当前值。

在这种情况下,由于defer语句可以访问并修改命名的返回值,所以它能够影响最终的返回值。

递归 todo

同java c定义 不再赘述

特点:深度逻辑上没有上限 调用栈不会出现溢出错误 (no more stackoverflow)

1
2
3
4
5
6
7
8
9
func a(){
a()
// 递归调用自身
}

func main(){
a()
// 进入递归函数
}

方法

定义声明方式见上方表格

go中没有java c++中类与对象的概念 但是它可以将方法和结构体绑定起来 从而实现面向对象的思想 如下

1
2
3
4
5
6
7
8
type aFile struct{
name string
}
func (a *aFile) Read() int {
// 在方法名的前方绑定了其所对应的类型aFile
fmt.Println("have read")
return 0
}

但是要注意 每种类型对应的方法必须和类型的定义在同一个包中 也因此int string这些内置的类型是无法添加方法的 在一定角度上也保持了安全性

其实这么整跟类也差不多 只是一个源文件可以塞很多进去(?)

go中不支持传统意义上的方法的重载 方法名在一个类型中必须是唯一的

接口

定义

概念大部分与其他语言相同 是对其所包含的类型行为的抽象和概括 与具体的实现方式无关

特点 & 空接口

注意 go中的接口是隐式规定的 一旦编写的函数是实现了某个接口中所定义的所有方法 一般就认为这个类与接口存在实现关系 下面是一个简易的接口与实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type aFile struct {
amount int
name string
}

func (a *aFile) Read() int {
fmt.Println("have read")
return 0
}

func (a *aFile) Write() int {
fmt.Println("have write")
return 0
}

func (a *aFile) Execute() int {
fmt.Println("have execute")
return 0
}

type file interface {
Read() int
Write() int
Execute() int
}

与其他语言一样 方法的返回值类型必须和接口所规定的一致 若达成了继承关系 goland中会有提示

但是如果我如下定义了一个空接口 这样就会导致所有的方法都符合实现的规则 也就是说此源文件内任意一个函数都是这个空接口的实现类

1
2
3
4
5
type emptyIface interface{
// 里面没有方法
// == 所有结构体都实现了其中的所有(0个)方法
// == 所有结构体都是这个接口的子类
}

不太严谨的说 可以将这个空接口理解成Object类

调用

1
2
3
4
func main() {
a := aFile{1, "2"}
b := a.Read()
}

如上

补充:go中没有明确的构造函数的概念

但是可以定义一个函数专门用于创建并初始化对应结构体的实例 以上方 aFile 为例子

1
2
3
4
5
6
func NewAFile *aFile(){
return &aFile{
amount: 0,
name: "default"
}
}

底层结构 todo

所谓interface本质上是一个结构体 可分为空接口带方法接口

空接口

1
2
3
4
struct Eface{
Type* type;
void* data;
}

带方法接口

1
2
3
4
5
struct Iface
{
Itab* tab;
void* data;
}

转换类型-赋值

在java中 接口是实现多态性的一利器 当将具体的数据类型赋值为对应的父接口时 需要进行类型的转换 java是在运行中转换的 而go是在编译的时候静态转换

按照上方描述 读者可以了解到空接口的特性 (见 特点&空接口)

转换为空接口

  • 返回Eface
  • data指向 数据
  • type指向 type结构体 (类比java中Type类型)

转换为非空接口

  • 检测目标接口的方法与数据结构体是否对应 (编译中进行)
  • 返回Iface

如何检测?

go中每一个具体类型或方法都有一个对应的方法表,编译中通过比较两表区别进行检测。

上方描述的是将具体类型值赋到接口类型当中,同理在将接口类型的值转换为具体类型时也需要进行类似的类型检测。见下方类型断言

类型断言

一种在运行时检查接口值的实际类型的机制。当我们需要从接口类型中获取具体的值并确定其类型时,就需要使用类型断言。

大致格式如下:

1
value, ok := x.(T)

假设x为接口 而T为需要转化的类型

  • 若类型为T value包含T的值 ok为true

  • 若类型非T valueT 类型的零值 ok为false

可类比成java中的 if+instanceof 的效果

也可以省略掉ok的值 但是在实际开发中 最好还是带上

如果没有进行类型断言 不能做什么

无法直接访问接口值中具体类型的方法或字段。

因为在使用接口时,编译器会丢失具体类型的信息,只保留了接口的静态类型信息

  1. 调用具体类型的方法: 如果一个接口值包含了一个具体类型的值,但你只是知道其接口类型,而没有进行类型断言,那么你将无法调用该具体类型的方法。

  2. 访问具体类型的字段: 类似地,如果一个接口值包含了一个具体类型的值,而你没有进行类型断言,你将无法直接访问该具体类型的字段。

详例见下方channel章中接口类型channel实例

channel

定义

一个传输数据的缓冲区 此通道遵循队列FIFO原则 保证数据的先来后到

某种程度上可以类比于java中的Buffer 和其具有一部分共通点

特点

  • 遵循FIFO原则

  • 在同一个通道中 只能同时有一个goroutine进行发送或获取数据的操作(单线程)

  • 线程安全 无需加锁

  • channel本身具有类型 可定义成父级接口类型方便存储

  • 需要make()后使用 容量不能二次改变(堆动态内存分配)

  • 每次接受单个数据元素

  • 要么被发送方阻塞 要么被接收方阻塞

声明

无缓冲channel
  • 同步性:无缓冲channel是同步的,这意味着发送操作会阻塞,直到另一个goroutine执行接收操作,反之亦然。
  • 直接通信:发送者和接收者必须同时准备好进行数据的发送和接收,因此无缓冲channel提供了一种强同步机制。
  • … (使用较少 不再介绍)
1
2
3
ch := make(chan Type)
//Type 为channel类型
ch := make(chan int)
有缓冲channel

普通channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main(){
var intChan chan int
intChan = make(chan int,2)

intChan <- 1
num := 2
intChan<- num
// 上方正常往通道放入两数据
intChan<- 3
// 通道放满了已经 没有办法继续放进去

// 从管道拿出两数据
var num2 int
num2 = <-intChan
num3 := <- intChan

// 此时通道已经没数据了 如果再取就会报告deadLock()

fmt.Println(len(intChan))
fmt.Println(cap(intChan))
// 可以使用这两个函数来确定
// 当前通道的长度(len)和容量(cap)
}

空接口类型channel (类型断言)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type Cat struct {
Name string
Age int
}

func main() {

//定义一个存放任意数据类型的管道 3个数据
//构造匿名空接口作为数据类型
allChan := make(chan interface{}, 3)


//可存入struct int string
allChan<- 10
allChan<- "tom jack"
cat := Cat{"小花猫", 4}
allChan<- cat

<-allChan
<-allChan

newCat := <-allChan
// !!!注意 此时管道取出的不是结构体 而是一个空接口类型的

fmt.Printf("newCat=%T , newCat=%v\n", newCat, newCat)
//下面的写法是错误的!编译不通过
//fmt.Printf("newCat.Name=%v", newCat.Name)
//使用类型断言
a := newCat.(Cat)
fmt.Printf("newCat.Name=%v", a.Name)
}

关于类型断言的再次说明 (详细概念见上方接口章节 此处仅根据例子说明)

这里传的是空接口类型的channel 如代码中可以接受任意类型的数据 但是取出来的时候每一个类型都必须要进行类型断言 不然无法获取其中的值

如上方的 a:= newCat.(Cat)

如果想要将int整型取出也是一样的 b:=c.(int)

数据发送与接收

数据发送
1
ch <- "hello world"

需要确保通道类型是数据类型或是数据的父接口类型

数据接受
  1. 阻塞接受数据
1
data := <-ch

阻塞当前线程(Goroutine)

  • 有数据 读取通道中的数据 将对应数据赋值给data

  • 无数据 阻塞直至通道中有数据读取 将对应数据赋值给data

  1. 非阻塞接收数据
1
data, ok := <-ch

又是熟悉的多变量赋值 当使用这一语句时

  • 若通道重量有数据 则data赋值为接受的数据 ok为true

  • 若通道无数据 data赋为该元素零值 ok为false (表示接受失败而不阻塞)

[^注意]:一般情况下 非阻塞方法通常结合for以轮询的方法查值 而阻塞则就只是阻塞 所以一般来说后者的效率更低 推荐使用前者 或结合select语句进行优化

发送者 & 接受者

此处先介绍channel作为通信机制两类主要的参与者

  • 发送者
发送者(Sender)
  1. 定义:向channel发送数据。
  2. 行为
    • 发送数据:通过channel <- value语法向channel发送值。
    • 关闭channel:发送者通常负责关闭channel,以通知接收者不会再有更多的数据发送。关闭是通过close(channel)实现的。
  3. 注意事项
    • 关闭后不能发送:一旦channel被关闭,再向它发送数据会引起panic。
    • 发送阻塞:如果没有接收者准备好接收数据,发送操作可能会阻塞。
接收者(Receiver)
  1. 定义:从channel接收数据。接收操作的类型必须与channel的类型匹配。
  2. 行为
    • 接收数据:通过value := <-channelvalue, ok := <-channel语法从channel接收值。
    • 检测channel关闭:使用value, ok := <-channel可以检测channel是否已关闭。如果okfalse,表示channel已经被关闭且没有更多的数据可接收。
  3. 注意事项
    • 接收阻塞:如果channel中没有数据可接收,接收操作会阻塞。
    • 关闭后继续接收:即使channel关闭,接收者仍可以接收到之前发送的数据。当channel中的数据都被接收完毕后,任何进一步的接收操作将不会阻塞并立即返回零值。

有关两者间协调问题可以借助select语句等实现 也可以设计多发送者或多接收者的模式 此处不再赘述

可参照Reactor模型思想

在实际的开发中 一般设计接收者和发送者为两个单独Goroutine分别负责(并发执行) 因为这种情况会导致死锁的出现

  • 对于无缓冲区 一旦放了数据 Goroutine就阻塞 运行不到下面的接受板块

  • 对于有缓冲区channel 同理也会在缓冲区满的时候出现这种状况

关流

使用close()函数关流 只能由发送者执行这句代码

关流之后不能写入 只能读出

1
2
3
close(channel)
// eg
close(ch)

循环遍历

!!如果不关流就遍历会出现死锁

channel也可以结合for range语句进行循环接受数据 并且只能使用一个变量来接受数据 即data

1
2
for data := range ch{
}

select语句

有点像switch但不是switch

设计目标 & 用法

处理并发编程中的多个channel操作 大多数专用于channel操作 允许在一组通信操作中等待任何一个操作完成 select 语句会阻塞,直到某个goroutine接收到数据。如果有多个通信操作准备就绪,select 会随机选择一个执行。

使用

大致语法与switch语句相似 不再赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch1 <- "Message from channel 1"
}()

go func() {
time.Sleep(1 * time.Second)
ch2 <- "Message from channel 2"
}()

select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout: No communication ready.")
}
}

上方代码中ch2更快能收到数据 select语句执行它 可结合for进行更复杂的操作

map

定义

用于存储键值对。它相当于其他语言中的字典或哈希表。广泛用于快速查找、数据存储和数据操作等

与其他高级语言相似 此处不再赘述

特点

  • 无序 当遍历返回一个map时 其返回的顺序是不确定的 每次打印出来的都不一样

  • 动态长度 可根据需要增大 添加时 会自动扩容

  • 引用类型 函数间传递不会产生副本 按址传递

关于动态长度

在下方声明map时 可指定初始容量 这个容量是哈希表的初始大小

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

在Go中,当 map 需要扩容时,它是通过以下步骤进行内存分配和重新散列的:

  1. 确定新大小:当 map 的元素数量接近当前容量的限制时,Go运行时会决定创建一个更大的哈希表。新表的大小通常是当前大小的两倍。这种增长策略旨在平衡内存使用和性能。
  2. 分配新内存:运行时为新的哈希表分配内存。新分配的内存会比之前的哈希表大,能够容纳更多的元素。
  3. 重新散列:现有的键值对需要被重新散列到新的哈希表中。这个过程涉及重新计算每个键的哈希值,并根据这个新的哈希值将键值对放入新表的相应位置。
  4. 迁移数据:旧的哈希表中的数据会被遍历,并且每个元素都会根据新的哈希函数和更大的桶数量被重新定位到新表中。
  5. 释放旧内存:一旦所有的键值对都被迁移到新的哈希表中,旧的哈希表所占用的内存会被释放回堆。

关键点

  • 性能影响:重新散列是一个相对昂贵的操作,因为它需要遍历整个 map 并对每个元素进行处理。这就是为什么在知道大致元素数量时预先设置一个足够大的初始容量是一个好的做法。
  • 渐进式扩容:在一些版本的Go中,为了减少单次扩容带来的性能冲击,引入了渐进式扩容。在这种机制中,扩容和数据迁移是逐渐进行的,在一系列的 map 操作中分摊了扩容的成本。
  • 不可预测性:由于哈希函数的特性,即使是小的改变也可能导致键值对在哈希表中的位置发生显著变化。因此,map 中元素的顺序是不可预测的,特别是在扩容之后。

声明

声明语句

1
var m map[KeyType]ValueType

初始化

1
2
3
4
5
6
7
8
9
m := make(map[KeyType]ValueType)
// or
m := map[KeyType]ValueType{
key1: value1,
key2: value2,
// ...
}
// eg
fruit := make(map[string]int)

基本操作

添加 更新

put()

1
2
3
4
5
m[key] = value
// eg
fruit["apple"] = 0
fruit["banana"] = 1
fruit["orange"] = 2
获取元素

get()

1
2
3
4
5
value := m[key]
// eg
int1 := fruit["apple"]
int2 := fruit["banana"]
int3 := fruit["orange"]
检查键是否存在

isContain()

1
2
3
4
5
6
value, ok := m[key]
// eg
int1, isFruit1 := fruit["apple"]
// isFruit是否存在 若存在isFruit为true 赋值int1
int2, isFruit2 := fruit["dog"]
// 不存在
删除元素

del()

1
2
3
delete(m,key)
// eg
delete(fruit,"apple")
遍历

iterator…..

1
2
3
4
5
6
for key, value := range m{
}
// eg
for intValue, fruitValue := range fruit{
fmt.Println("Key:",intValue,"Value:",fruitValue)
}

代码块 for key, value := range ... 中的keyvalue分别代表每一次迭代中的的键和值