闭包
总结一下就是函数作为参数/返回值,利用其特性。 闭包的原理:函数及其引用的外部变量的组合。
特点
-
函数嵌套: 闭包是一个函数内部定义的函数,也就是说,一个函数可以嵌套在另一个函数的内部。
-
引用外部变量: 闭包函数可以引用其外部函数中定义的变量,即使外部函数已经执行完毕,闭包仍然可以访问这些变量。
用途&优势
-
数据隐藏和封装: 闭包允许将某些变量隐藏在函数的作用域内,只暴露必要的接口,从而实现数据封装和隐藏的目标。
-
保留状态: 闭包可以在函数调用之间保留状态。例如,在计数器应用中,可以使用闭包来保留计数状态,而无需全局变量。
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
}
-
回调函数: 闭包可以用作回调函数,将一段逻辑传递给其他函数,以在特定条件满足时执行。
-
函数工厂: 闭包可以用来创建一组具有相似行为但具有不同状态的函数,实现所谓的“函数工厂”。
package main
import "fmt"
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println("Double:", double(5)) // 输出: 10
fmt.Println("Triple:", triple(5)) // 输出: 15
}
- 高阶函数: 允许将函数作为参数传递给其他函数,以及从函数中返回函数。这种功能在函数式编程中非常重要。
package main
import "fmt"
func applyFunc(nums []int, f func(int) int) []int {
result := make([]int, len(nums))
for i, num := range nums {
result[i] = f(num)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
double := func(x int) int { return x * 2 }
squared := func(x int) int { return x * x }
doubledNumbers := applyFunc(numbers, double)
squaredNumbers := applyFunc(numbers, squared)
fmt.Println("Doubled:", doubledNumbers) // 输出: [2 4 6 8 10]
fmt.Println("Squared:", squaredNumbers) // 输出: [1 4 9 16 25]
}
Embedding
嵌入,表达一种组合类型
type container struct {
base//一个 `container` _嵌入_ 了一个 `base`. 一个嵌入看起来像一个没有名字的字段
str string
}
co := container{
base: base{
num: 1,
},
str: "some name",
}//当创建含有嵌入的结构体,必须对嵌入进行显式的初始化; 在这里使用嵌入的类型当作字段的名字
泛型
即类型参数
func MapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
作为泛型函数的示例,MapKeys 接受任意类型的Map并返回其Key的切片。 这个函数有2个类型参数 - K 和 V; K 是 comparable 类型,也就是说我们可以通过 == 和 != 操作符对这个类型的值进行比较。这是Go中Map的Key所必须的。 V 是 any 类型,意味着它不受任何限制 (any 是 interface{} 的别名类型).类似于C++里面template?
any 是 interface{}的别名类型
数据序列化与 SQL 表达式
SQL 表达式更新创建
- 通过 gorm. Expr
- 使用 GORMValuer
- 通过*gorm. DB 使用 SubQuery
查询
- gorm. Expr
- struct 定义 GormValuer
- 自定义查询 SQL 实现接口 clause. Expression
Goroutine
从 Java 的角度实践 Go 工程| 青训营笔记 - HikariLan's Blog
协程:用户态,轻量级线程,栈 KB 级别 有栈协程 //go 适合高并发的关键所在 进程:内核态,线程跑多个协程,栈 MB 级别
意义: 如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是 I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理机器上使用并发就没有任何意义。
函数前加 go 关键字开启 提倡通信共享内存,而不是共享内存通信(影响性能
多个 goroutine 可以同时访问和修改共享的数据的原因在于,Go 运行时系统会在 goroutine 之间进行自动的抢占式调度。这意味着一个 goroutine 在执行过程中可能会被中断,然后另一个 goroutine 被调度执行。这种调度发生的时间点是不确定的,取决于操作系统线程的调度和 Go 运行时系统的策略。
适用场景:
- 协程:适用于需要高并发和轻量级并行的场景,如网络通信、I/O 密集型任务等。
- 线程:适用于需要利用多核处理器和进行计算密集型任务的场景。
上下文 Context
用来设置截至日期、同步信号、传递请求相关值的结构体,控制 goroutinue 的运行,超时,取消方法的调用。 替代 控制运行 select+channel 超时控制 ticker 取消方法调用 向 channel 中发送信号,通知方法退出
context 的优点是在复杂场景下方便控制 场景 1 主协程启动了 m 个子协程,分别编号为 g1,g2,...gm。对于 g1协程,它又启动了 n 个子协程,分别编号为 g11,g12,...g1n。现在希望主协程取消的时候或 g1取消的时候,g1下面的所有子协程也取消执行,采用 channel 的方法,需要申请2个 channel, 一个是主协程退出通知的 channel,另一个是 g1退出时的 channel。g1的所有子协程需要同时 select 这2个 channel。现在是2层,用 channel 还能接受,如果层级非常深,那监控起来需要很多的 channel, 操作非常繁琐。 场景 2 微服务中,任务 A 运行依赖于下游的任务 B, 考虑到任务 B 可能存在服务不可用,所以通常在任务 A 中会加入超时返回逻辑,需要开一个定时器,同时任务 A 也受控于父协程,当父协程退出时,希望任务 A 也退出,那么在任务 A 中也要监控父协程通过 channle 发送的取消信息。
Context 接口一共包含四个方法:
- Deadline:返回绑定该context任务的执行超时时间,若未设置,则ok等于false
- Done:返回一个只读通道,当绑定该context的任务执行完成并调用cancel方法或者任务执行超时时候,该通道会被关闭
- Err:返回一个错误,如果Done返回的通道未关闭则返回nil,如果context如果被取消,返回
Canceled错误,如果超时则会返回DeadlineExceeded错误 - Value:根据 key 返回,存储在 context 中 k-v 数据
type Context interface {
// 返回这个Context被取消的截止时间,如果没有设置截止时间,ok的值返回的是false,
// 后续每次调用对象的Deadline方法是,返回的结果都和第一次相同,即具有幂等性
Deadline() (deadline time.Time, ok bool)
// 返回一个channel对象,在Context被取消时,此channel会被close。如果没有被
// 取消,可能返回nil。每次调用Done总是会返回相同的结果,当channel被close的时候,
// 可以通过ctx.Err获取错误信息
Done() <-chan struct{}
// 返回一个error对象,当channel没有被close的时候,Err方法返回nil,如果channel被close, Err方法会返回channel被close的原因,可能是被cancel,deadline或timeout取消
Err() error
// 返回此cxt中指定key对应的value
Value(key interface{}) interface{}
}
Background/Todo
background 和 todo 是两个全局 Context,实现方式都是返回 nil 值,两者都不可导出,通过包提供的 Background()和 TODO()导出供外部使用,两者都是不可取消的 Context,通常都是放在 main 函数或者最顶层使用。 深入解析Golang之context - 知乎
Channel
make(chan 元素类型,[缓冲大小*]) *不填无缓冲通道,反之有缓冲通道 创建:ch:=make (chan int, 3) 发送:ch<-v 接受:v:=<-ch 关闭:close (ch)
通道同步
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
} //`done` 通道将被用于通知其他协程这个函数已经完成工作。
func main() {
done := make(chan bool, 1)
go worker(done)
<-done
}//如果你把 `<- done` 这行代码从程序中移除, 程序甚至可能在 `worker` 开始运行前就结束了。
通道方向
当使用通道作为函数的参数时,可以指定这个通道是否为只读或只写。
func pong(pings <-chan string, pongs chan<- string)
pings只读通道,pongs只写通道
通道选择器
选择器(select) 让你可以同时等待多个通道操作。 各个通道将在一定时间后接收一个值, 通过这种方式来模拟并行的协程执行(例如,RPC 操作)时造成的阻塞(耗时)。
匿名函数
func(j int) {
hello(j)
}(i)
}
声明了一个int类型的形参j,()传入实参i
WaitGroup
-
通过调用
Add方法,向WaitGroup的计数器添加指定值; -
通过调用
Wait方法阻塞当前协程,这会使得协程陷入无限的等待; -
通过调用
Done方法使WaitGroup内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。
锁
for i := 0; i < 5; i++ {
go addWithLock()
}
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
并发锁的原理是,当第一次调用 Lock 方法时,什么都不会发生,但当第二次调用 Lock 方法时,该调用便会立刻阻塞协程,直到有程序调用 Unlock 方法解锁。
并发锁的使用实际上将并行的程序串行化,会导致显著降低性能;同时,不当的锁使用也可能导致死锁(DeadLock)等问题发生。
对于单个值的修改而不是一段代码的执行,更推荐引入标准库中的 sync/atomic 包来实现。
杂
在 Go 中,nil 是一个预定义的标识符,用于表示指针、切片、映射、通道、函数和接口类型的零值。它表示该值没有指向任何对象或者说它是一个空值。 具体地说: 对于指针类型,nil 表示指针没有指向任何有效的内存地址。Copy 对于切片、映射、通道和函数类型,nil 表示它们没有被分配和初始化。 对于接口类型,nil 表示接口没有被指定具体的类型和值。 需要注意的是,只有指针、切片、映射、通道、函数和接口类型才能被赋值为 nil,其他数据类型(例如 int、string、bool)不能赋值为 nil。