1. defer
defer作为GO中的一个关键字,它的作用就是延迟执行一个函数,直到当前函数执行完毕之后,defer延迟执行的函数才会执行。
非常有利于处理一些特殊的情况,例如当进行文件操作的时候,我们就可以defer对应的文件关闭操作,从而确保即使对应的代码发生问题,出错了,但是对应的文件还是可以正常关闭的,避免一些问题的出现,通常有以下用途:
- 资源释放:比如文件关闭、数据库连接关闭、锁释放等。
- 异常恢复:在 Go 中没有
try-catch,但可以用defer+recover()来处理异常。 - 日志记录:函数结束时自动记录日志。
- 跟踪性能:可以通过
defer来记录某个函数的执行时间。
同时也有一些特点,例如:
|
延迟执行 |
后面的函数会延迟到当前函数返回时执行。 |
|
执行顺序 |
如果有多个 ,它们会逆序执行(LIFO)。 |
|
常见用途 |
资源清理(如关闭文件、数据库连接、释放锁)、错误恢复、日志记录等。 |
|
性能开销 |
有轻微的性能开销,但通常被认为是为了清晰的代码和避免资源泄漏而值得使用。 |
2. 指针
GO的指针可以说非常的重要,有了这个东西我们就可以避免很多的值复制,之后直接通过指针获取对应的值
指针的声明:可以通过:var ptr *int // 声明一个指向 int 类型的指针
也就是 * 专属符号,通过这个符号声明当前的变量是真么样的指针
指针的指向:
即我们的指针应该指向哪一个位置?指针,实际上指的就是一个对应数据值的地址,我们一般用:&符号来表示当前指针的位置指向,例如:
a := 1
p := &a // 获取指向a地址值的指针
获取指针指向的的具体值:
fmt.Println(*p) // 使用 *p 获取对应指针位置的值
采用符号 * 可以直接获取对应指向指定位置指针的数据值
3. 结构体
类似于JAVA当中的类了
a. 创建语法
type [structName] struct {
// 填充对应的字段属性
X ing
Y int
}
b. 初始化方式
其实就是有参构造,还是无参构造,以及创建之后结构体内部的值如何处理
有以下几种访问方式:
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予零值 隐式的创建
v3 = Vertex{} // X:0 Y:0 给出初始化的值
c. 结构体使用方式
其实跟JAVA差不多,直接拿来用就行,但是没有了构造函数,还有对应的成员方法这一说,如下:
// 结构体的创建,就是一系列的属性的集合
// 这个其实就跟类差不多,内部有对应的成员变量
type Vertex struct {
name string
age int
gender byte
}
func main() {
fmt.Println(Vertex{"张三", 1, 0})
}
d. 字段属性的获取
GO当中,可以通过 . + 字段命的方式直接获取即可
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
4. 数组
跟JAVA当中的数组一样,但是还是需要提一些:
GO当中,数组的长度跟GO本身是写死到一起的,也就是说即使是相同类型的数组,长度不一样,那么就不是一个一样的数组
另外,GO当中,数组的长度一旦被固定,那么永远都不能被修改
a. 数组的声明方式:
对于一维数组来说:
同时还涉及到了结构体的声明,大差不差都是这样
全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部:
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
对于二维数组来说,其实也是类似的
全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
5. 切片
针对于一般的数组只能固定长度,然后进行声明,这种方式显然是不灵活的
所以就有了切片,可以动态的调整数组的大小
在 Go 语言中,切片(slice) 是一种非常重要的数据结构,它是 对数组的一个动态视图,提供了比数组更加灵活的功能。切片不仅能动态扩展长度,还能方便地引用和操作底层数组的部分数据。
但是有一点非常需要注意,切片本身是不包含任何的数据的,不占用任何的内容的,也就是说:切片就像数组的引用,并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
和它共享底层数组的切片都会观测到这些修改。
a. 切片的声明
var s []int = []int
s := []int{1,2,4}
s := make([]int,len,cap)
主要就是这三种切片的声明方式
其中,make方法专门的就是来声明切片或者说MAP数组这种的的初始化的
其下有三个参数,一个是对应类型,例如 []int
第二个是 len() 长度,代表切片长度
第三个是cap 容量,代表对应切片容量
针对切片我们一般不对其设置长度以及cap,而是根据切实的数组去设置,所以都是0 0
/**
* make([]T, length, capacity)
* type: 切片的数据类型
* length: 切片的长度,也就是实际包含的元素的数量
* capacity: 容量 超过容量的是扩容,扩容的方式是翻倍扩容,增加为原来容量的2倍
* 但是并不是无限制的增长,而是有一定的空间限制,如果现在的容量已经很大的前提下,就不会翻倍了
* 而是会缩小扩大的比例,避免一直翻倍扩容
*/
另外,切片声明之后,如果对应的初始长度为0,那么这个时候直接对切片修改是会报错的,因为长度为0,没有办法对对应位置的值修改
b. 切片追加
这里的追加指的是,向切片添加原本数组可能没有的元素
官方提供了 append 方法,能够让我们向切片内加入新的元素
var s []int
printSlice(s)
// 可在空切片上追加
s = append(s, 0)
printSlice(s)
需要注意的一点是,切片容量的扩展
//切片容量扩展
slice := make([]int, 0, 5)
slice = append(slice, 1, 2, 3, 4, 5)
printSlice(slice)
// len=5 cap=5 slice=[1 2 3 4 5]
// 再新增
slice = append(slice, 6)
printSlice(slice)
// 容量扩展了一倍,为10
一般来说,切片扩展的大小都会是原本切片的倍数,在案例中原本是5之后扩展为了10
c. 遍历方式
索引的遍历方式,可以采用 range 可以自主的遍历切片
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
// range 两个参数,第一个为对应的索引位置,第二个为对应的值,跟MAP一样
for i,v := range pow {
fmt.Printf("索引为:%d,对应的值是:%d\n",i,v)
}
}
这里可以当作是,MAP一样,有两个参数,一个是:
i 代表索引;
v 代表值
也可以忽略它,直接获取值,就用到了 _
例如
for i, _ := range pow
for _, value := range pow
例如:
pow := make([]int, 10)
for i := range pow {
// 向左边移动多少位
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
6. MAP
a. 声明以及初始化
映射的零值为 nil 。nil 映射既没有键,也不能添加键。
make 函数会返回给定类型的映射,并将其初始化备用。
type Vertex struct {
Lat, Long float64
}
// MAP的声明
// 第一个代表对应的key类型,第二个代表对应的V的类型,这里是又定义了一个结构
var m map[string]Vertex
// MAP的初始化
一般都使用make对其初始化
m = make(map[string]Vertex)
// 初始化了之后就可以自主添加值了
b. 键值对的添加以及获取
在括号内写对应键,外部写值即可,如下:
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
// 通过值获取对应键
fmt.Println(m["Bell Labs"])
c. 删除元素
delete(m, key)
// 例如:
// 直接声明 + 初始化
m := make(map[string]int)
m["答案"] = 42
fmt.Println("值:", m["答案"])
delete(m, "答案")
d. 检测某一个键是否存在
GO是通过双值检查的,语法如下:
elem, ok = m[key]
// 检测对应的KEY是否存在,如果存在,elem会展示对应的值,不存在就为对应类型的默认值
v , ok := m["问题1"]
fmt.Println("值:", v, "是否存在?", ok)
这里分享一个strings工具类,能够把对应的字符串直接以单词划分一个string切片
7. 函数值
其实,我们在上面学函数的时候,函数不仅能作为一个方法去调用,其实也可以作为一个值,让其他函数去调用,这就是函数值,例如:
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
// 定义一个函数值
// 这里其实就是跟视图差不多,跟正常的方法的定义一样,但是怎么执行的以及哪里执行我们手动控制
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
我们可以直接在一个函数里面定义一个函数作为值,之后直接去使用,甚至函数里面也能嵌套函数的结果作为值
8. 函数闭包
为什么叫闭包呢?闭的什么?关的什么?
闭包,最大的特点就是:能够将当前函数作用范围外的各种环境,以及变量将自己结合为一体,所以叫闭包,从而形成了一个封闭的结构
- 闭:指的是该函数和它的作用域“封闭”在一起。函数不仅仅有自己的参数和局部变量,还可以捕获和保存外部函数的变量。
- 包:这个词则是指它对外部环境的“封装”,也就是说,闭包将外部作用域的变量“带到”它自己的作用域中
所以,闭包实际上就是一个函数,只不过是具有了一些特殊的性质,也就是上面说的。
a. 闭包的使用
其实很简单,前面我们其实就已经学过了,函数就是闭包,只不过可能没有闭包的一些特性,如下:
func adder() func(int) int {
sum := 0
return func(x int) int {
// 函数值
addFunc := func(sum,x int) int {
return sum + x
}
sum = addFunc(sum,x)
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
// 两个独立的函数闭包
neg(-3*i),
)
}
}
在这里例子当中,我们使用的函数中包含了函数外的值,sum,即使当前函数 adder 执行完毕了,这个 sum 变量也应该被销毁了
但是!我们在内部使用了这个值,那么就会一直保留下去,我们之后调用返还的值的时候,还是能调用并且使用看似已经被销毁了的值!
b. 闭包的特性
- 捕获外部变量:闭包能够访问并修改外部函数的局部变量,即使外部函数已经执行完毕并返回。
- 延续生命周期:即使外部函数已经结束,闭包仍然可以访问和操作这些变量,因为它们被保存在闭包内部。
c. 作用
因为闭包的一些特性,也就是即使当前方法已经被执行完毕,但是如果其内部的一些函数引用了外部环境的一些变量,那么还是能继续使用的,一直都保存的有外部环境。
基于此,一些情况下我们就不需要声明对应的全局变量,而是局部变量就能做到一样的功效,初次之后,还有以下作用:
-
- 保持状态:
闭包可以让你在不使用全局变量的情况下,保持某些状态。例如,你可以利用闭包创建一个函数工厂,每次调用工厂时都可以创建一个具有某些特定初始状态的函数。
package main
import "fmt"
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c := counter() // 创建闭包
fmt.Println(c()) // 输出 1
fmt.Println(c()) // 输出 2
fmt.Println(c()) // 输出 3
}
- 函数式编程:
Go 是一种支持函数式编程的语言,闭包可以让我们更方便地使用函数作为参数或返回值。例如,你可以创建一个接受回调函数的函数,并在闭包中定义回调逻辑。
package main
import "fmt"
func applyOperation(x int, operation func(int) int) int {
return operation(x)
}
func main() {
addTwo := func(x int) int {
return x + 2
}
result := applyOperation(5, addTwo)
fmt.Println(result) // 输出 7
}
在这个例子中,applyOperation 函数接受一个 operation 闭包作为参数。我们定义了一个 addTwo 闭包,将其传递给 applyOperation 来进行操作。
- 延迟执行和异步操作:
闭包在延迟执行和异步操作中非常有用,特别是在并发编程中,你可以利用闭包封装任务并将其传递给 goroutine 或定时器。
package main
import "fmt"
import "time"
func main() {
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println("Task", i)
}(i) // 将 i 作为参数传递给闭包
}
time.Sleep(time.Second)
}
在这个例子中,go 语句启动了三个并发任务,每个任务都在闭包中执行。闭包捕获了每次循环时的 i 值,确保每个任务输出正确的结果。
2万+

被折叠的 条评论
为什么被折叠?



