-
在讲解闭包之前先看一下 Golang 中的匿名函数。匿名函数(Anonymous Functions)匿名函数也可以称为函数字面量、lambda 函数或者闭包。闭包的概念起源于 lambda 微积分中表达式的数学求值。从技术上讲,匿名函数和闭包之间有细微的区别:匿名函数是没有名称的函数,而闭包则是函数的实例。在 Golang 中要实现闭包,是离不开匿名函数的。先看一个普通函数的例子,例如:func add(x, y int) { fmt.Println(x + y) }调用方式如下:add(1, 2) // 输出 3接下来看下如何使用匿名函数来实现相同的功能:func(x, y int) { fmt.Println(x + y) }(1, 2)这个匿名函数和上面的普通的函数的功能是一样的,区别是没有名字定义之后就直接调用接下来,使用通过创建一个返回一个函数的函数的方式来使用一个匿名函数。函数一般都是返回整数、字符串、结构体等基本类型,但是在 Golang 中一个函数可以返回另一个函数。如下是 Golang 官方的一个例子:func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } }这个函数的返回类型是 func(int) int 类型的函数,可以将这个函数的返回值赋值给一个变量,然后可以像调用一个函数的方式使用调用这个变量,例如:pos := adder() pos(1)闭包(Closures)通过上文的讲解我们已经知道了匿名函数的定义以及使用方式,也了解了一个函数可以返回另一个函数,接下来讲解下闭包。在 Golang 中,闭包是一个引用了作用域之外的变量的函数。闭包的存在时间可以超过创建它的作用域,因此它可以访问该作用域中的变量,即使在该作用域被销毁之后。上文中的 adder() 函数返回的就是一个典型的闭包。Golang 中的匿名函数也被称为闭包,匿名函数是一种特殊类型的函数,没有名称,闭包可以被认为是一种特殊类型的匿名函数。Golang 中的闭包由两部分组成:函数体和函数执行时的上下文环境。函数体定义了闭包的逻辑,上下文环境则包含了函数外部的变量。当闭包被创建时,会将外部变量的引用保存在上下文环境中,并且在函数体内部可以随时访问这些外部变量。看个将上文中的 adder() 函数稍作修改的例子:package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { fmt.Println("执行前 sum =", sum) sum += x return sum } } func main() { pos := adder() for i := 0; i < 4; i++ { fmt.Println("执行后 sum =", pos(1)) } }运行结果如下:执行前 sum = 0执行后 sum = 1执行前 sum = 1执行后 sum = 2执行前 sum = 2执行后 sum = 3执行前 sum = 3执行后 sum = 4可以看出,闭包函数引用的外部变量被保存在了上下文环境中(一直不被销毁),每次执行闭包,闭包内的变量又保存了上一次运行后的值。
-
1. 引言io.ReadAtLeast 函数是Go标准库提供的一个非常好用的函数,能够指定从数据源最少读取到的字节数。本文我们将从io.ReadAtLeast 函数的基本定义出发,讲述其基本使用和实现原理,以及一些注意事项,基于此完成对io.ReadAtLeast 函数的介绍。2. 基本说明2.1 基本定义io.ReadAtLeast 函数用于从读取器(io.Reader)读取至少指定数量的字节数据到缓冲区中。函数定义如下:1func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)其中r 是数据源,从它读取数据,而buf是用于接收读取到的数据的字节切片,min是要读取的最小字节数。io.ReadAtLeast 函数会尝试从读取器中最少读取 min 个字节的数据,并将其存储在 buf 中。2.2 使用示例下面是一个示例代码,演示如何使用 io.ReadAtLeast 函数从标准输入读取至少 5 个字节的数据:package main import ( "fmt" "io" "os" ) func main() { buffer := make([]byte, 10) n, err := io.ReadAtLeast(os.Stdin, buffer, 5) if err != nil { fmt.Println("读取过程中发生错误:", err) return } fmt.Printf("成功读取了 %d 个字节:%s\n", n, buffer) }在这个例子中,我们创建了一个长度为 10 的字节切片 buffer,并使用 io.ReadAtLeast 函数从标准输入读取至少 5 个字节的数据到 buffer 中。下面是一个可能的输出,具体如下:12hello,world成功读取了 10 个字节:hello,worl这里其指定 min 为5,也就是最少读取5个字节的数据,此时调用io.ReadAtLeast函数一次性读取到了10个字节的数据,此时也满足要求。这里也间接说明了io.ReadAtLeast只保证最少要读取min个字节的数据,但是并不限制更多数据的读取。3. 实现原理在了解了io.ReadAtLeast 函数的基本定义和使用后,这里我们来对io.ReadAtLeast 函数的实现来进行基本的说明,加深对io.ReadAtLeast 函数的理解。其实 io.ReadAtLeast 的实现非常简单,其定义一个变量n, 保存了读取到的字节数,然后不断调用数据源Reader中的 Read 方法读取数据,然后自增变量n 的值,直到 n 大于 最小读取字节数为止。下面来看具体代码的实现:func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) { // 传入的缓冲区buf长度 小于 最小读取字节数min的值,此时直接返回错误 if len(buf) < min { return 0, ErrShortBuffer } // 在 n < min 时,不断调用Read方法读取数据 // 最多读取 len(buf) 字节的数据 for n < min && err == nil { var nn int nn, err = r.Read(buf[n:]) // 自增 n 的值 n += nn } if n >= min { err = nil } else if n > 0 && err == EOF { // 读取到的数据字节数 小于 min值,同时数据已经全部读取完了,此时返回 ErrUnexpectedEOF err = ErrUnexpectedEOF } return }4. 注意事项4.1 注意无限等待情况的出现从上面io.ReadAtLeast 的实现可以看出来,如果一直没有读取到指定数量的数据,同时也没有发生错误,将一直等待下去,直到读取到至少指定数量的字节数据,或者遇到错误为止。下面举个代码示例来展示下效果:func main() { buffer := make([]byte, 5) n, err := io.ReadAtLeast(os.Stdin, buffer, 5) if err != nil { fmt.Println("读取过程中发生错误:", err) return } fmt.Printf("成功读取了 %d 个字节:%s\n", n, buffer) }在上面代码的例子中,会调用io.ReadAtLeast 函数从标准输入中读取 5 个字节的数据,如果标准输入一直没有输够5个字节,此时这个函数将会一直等待下去。比如下面的这个输入,首先输入了he两个字符,然后回车,由于还没有达到5个字符,此时io.ReadAtLeast函数一直不会返回,只有再输入llo这几个字符后,才满足5个字符,才能够继续执行,所以在使用io.ReadAtLeast函数时,需要注意无限等待的情况。he llo 成功读取了 5 个字节:he ll4.2 确保 buf 的大小足够容纳至少 min 个字节的数据在调用io.ReadAtLeast函数时,需要保证缓冲区buf的大小需要满足min,如果缓冲区的大小比 min 参数还小的话,此时将永远满足不了 最少读取 min个字节数据的要求。从上面io.ReadAtLeast 的实现可以看出来,如果其发现buf的长度小于 min,其也不会尝试去读取数据,其会直接返回一个ErrShortBuffer 的错误,下面通过一个代码展示下效果:func main() { buffer := make([]byte, 3) n, err := io.ReadAtLeast(os.Stdin, buffer, 5) if err != nil { fmt.Println("读取过程中发生错误:", err) return } fmt.Printf("成功读取了 %d 个字节:%s\n", n, buffer) }比如上述函数中,指定的buffer的长度为3,但是io.ReadAtLeast要求最少读取5个字节,此时buffer并不能容纳5个字节的数据,此时将会直接ErrShortBuffer错误,如下:读取过程中发生错误: short buffer5. 总结io.ReadAtLeast函数是Go语言标准库提供的一个工具函数,能够从数据源读取至少指定数量的字节数据到缓冲区中。 我们先从 io.ReadAtLeast 函数的基本定义出发,之后通过一个简单的示例,展示如何使用io.ReadAtLeast函数实现至少读取指定字节数据。接着我们讲述了io.ReadAtLeast函数的实现原理,其实就是不断调用源Reader的Read方法,直接读取到的数据数满足要求。在注意事项方面,则强调了调用io.ReadAtLeast 可能出现无限等待的问题,以及需要确保 buf 的大小足够容纳至少 min 个字节的数据。
-
应用闭包在程序 function_return.go 中我们将会看到函数 Add2 和 Adder 均会返回签名为 func(b int) int 的函数:func Add2() (func(b int) int) func Adder(a int) (func(b int) int)函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。我们也可以将 Adder 返回的函数存到变量中(function_return.go)。package main import "fmt" func main() { // make an Add2 function, give it a name p2, and call it: p2 := Add2() fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3)) // make a special Adder function, a gets value 2: TwoAdder := Adder(2) fmt.Printf("The result is: %v\n", TwoAdder(3)) } func Add2() func(b int) int { return func(b int) int { return b + 2 } } func Adder(a int) func(b int) int { return func(b int) int { return a + b } }输出:Call Add2 for 3 gives: 5The result is: 5下例为一个略微不同的实现(function_closure.go):package main import "fmt" func main() { var f = Adder() fmt.Print(f(1), " - ") fmt.Print(f(20), " - ") fmt.Print(f(300)) } func Adder() func(int) int { var x int return func(delta int) int { x += delta return x } }函数 Adder() 现在被赋值到变量 f 中(类型为 func(int) int)。输出:1 - 21 - 321三次调用函数 f 的过程中函数 Adder() 中变量 delta 的值分别为:1、20 和 300。我们可以看到,在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1,然后 1 + 20 = 21,最后 21 + 300 = 321:闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。这些局部变量同样可以是参数,例如之前例子中的 Adder(as int)。这些例子清楚地展示了如何在 Go 语言中使用闭包。在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:var g int go func(i int) { s := 0 for j := 0; j < i; j++ { s += j } g = s }(1000) // Passes argument 1000 to the function literal.这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。学习并理解以下程序的工作原理:一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:func MakeAddSuffix(suffix string) func(string) string { return func(name string) string { if !strings.HasSuffix(name, suffix) { return name + suffix } return name } }现在,我们可以生成如下函数:addBmp := MakeAddSuffix(".bmp") addJpeg := MakeAddSuffix(".jpeg")然后调用它们:addBmp("file") // returns: file.bmp addJpeg("file") // returns: file.jpeg可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点。我们已经在第 6.7 中得知函数也是一种值,因此很显然 Go 语言具有一些函数式语言的特性。闭包在 Go 语言中非常常见,常用于 goroutine 和管道操作
-
1. 引言io.discard是Go语言标准库提供一个结构体类型,其在丢弃不需要的数据场景下非常好用。本文我们将从io.discard 类型的基本定义出发,讲述其基本使用和实现原理,接着简单描述 io.discard 的使用场景,基于此完成对 io.discard 类型的介绍。2. 介绍2.1 基本定义io.discard 是 Go语言提供的一个Writer,这个Writer 比较特殊,其不会做任何事情。它会将写入的数据立即丢弃,不会做任何处理。其定义如下:type discard struct{} func (discard) Write(p []byte) (int, error) {} func (discard) WriteString(s string) (int, error) {} func (discard) ReadFrom(r Reader) (n int64, err error) {}discard 结构体类型没有定义任何字段,同时还提供了Write ,ReadFrom和WriteString 方法,Write 方法和WriteString 方法分别接收字节切片和字符串,然后返回写入的字节数。同时还实现了io.ReaderFrom 接口,这个是为了在使用 io.Copy 函数时,将数据从源复制到io.discard 时,避免不必要的操作。从上面discard 的定义可以看起来,其不是一个公开类型的结构体类型,所以我们并不能创建结构体实例。事实上Go语言提供了一个io.discard 实例的预定义常量,我们直接使用,无需自己创建实例,定义如下:var Discard Writer = discard{}2.2 使用说明下面通过一个丢弃网络连接中不再需要的数据的例子,来展示io.Discard 的使用,代码示例如下:package main import ( "fmt" "io" "net" "os" ) func discardData(conn net.Conn, bytesToDiscard int64) error { _, err := io.CopyN(io.Discard, conn, bytesToDiscard) return err } func main() { conn, err := net.Dial("tcp", "example.com:80") if err != nil { fmt.Println("连接错误:", err) return } defer conn.Close() bytesToDiscard := int64(1024) // 要丢弃的字节数 err = discardData(conn, bytesToDiscard) if err != nil { fmt.Println("丢弃数据错误:", err) return } fmt.Println("数据已成功丢弃。") }在上面示例中,我们建立了网络连接,然后连接中的前1024个字节的数据是不需要的。这个时候,我们通过io.CopyN 函数将数据从conn 拷贝到io.Discard 当中,基于io.Discard 丢弃数据的特性,成功将连接的前1024个字节丢弃掉,而不需要自定义缓冲区之类的操作,简单高效。3. 实现原理io.Discard的目的是在某些场景下提供一个满足io.Writer接口的实例,但用户对于数据的写入操作并不关心。它可以被用作一个黑洞般的写入目标,默默地丢弃所有写入它的数据。所以io.discard 的实现也相对比较简单,不对输入的数据进行任何处理即可,下面我们来看具体的实现。首先是io.discard 结构体的定义,没有定义任何字段,因为本来也不需要执行任何写入操作:type discard struct{}而对于Write 和 WriteString 方法,其直接返回了传入参数的长度,往该Writer 写入的数据不会被写入到其他地方,而是被直接丢弃:func (discard) Write(p []byte) (int, error) { return len(p), nil } func (discard) WriteString(s string) (int, error) { return len(s), nil }同时discard 也实现了io.ReaderFrom 接口,实现了ReadFrom 方法,实现也是非常简单,从blackHolePool 缓冲池中获取字节切片,然后不断读取数据,读取完成之后,再将字节切片重新放入缓冲池当中:// 存在一个字节切片缓冲池 var blackHolePool = sync.Pool{ New: func() any { b := make([]byte, 8192) return &b }, } func (discard) ReadFrom(r Reader) (n int64, err error) { // 从缓冲池中取出一个 字节切片 bufp := blackHolePool.Get().(*[]byte) readSize := 0 for { // 不断读取数据,bufp 只是作为一个读取数据的中介,读取到的数据并无意义 readSize, err = r.Read(*bufp) n += int64(readSize) if err != nil { // 将字节切片 重新放入到 blackHolePool 当中 blackHolePool.Put(bufp) if err == EOF { return n, nil } return } } }在io.Copy 函数中,将调用discard 中的ReadFrom 方法,能够将Writer中的所有数据读取完,然后丢弃掉。4. 使用场景io.Discard 给我们提供了一个io.Writer 接口的实例,同时其又不会真实得写入数据,这个在某些场景下非常有用。有时候,我们可能需要一个实现io.Writer 接口的实例,但是我们并不关心数据写入Writer 的结果,也不关心数据是否写到了哪个地方,此时io.Discard 就给我们提供了一个方便的解决方案。同时io.Discard 可以作为一个黑洞写入目标,能够将数据默默丢弃掉,不会进行实际的处理和存储。所以如果我们想要丢弃某些数据,亦或者是需要一个io.Writer接口的实例,但是对于写入结果不需要关注时,此时使用io.Discard 是非常合适的。5. 总结io.discard 函数是Go语言标准库中一个实现了Writer接口的结构体类型,能够悄无声息得实现数据的丢弃。 我们先从io.discard 类型的基本定义出发,之后通过一个简单的示例,展示如何使用io.discard 类型实现对不需要数据的丢弃。接着我们讲述了io.discard 类型的实现原理,其实就是不对写入的数据执行任何操作。在使用场景下,我们想要丢弃某些数据,亦或者是需要一个io.Writer接口的实例,但是对于写入结果不需要关注时,此时使用io.Discard 是非常合适的。基于此,便完成了对io.discard 类型的介绍,希望对你有所帮助。
-
本文主要研究一下tunnyWorkertype Worker interface { // Process will synchronously perform a job and return the result. Process(interface{}) interface{} // BlockUntilReady is called before each job is processed and must block the // calling goroutine until the Worker is ready to process the next job. BlockUntilReady() // Interrupt is called when a job is cancelled. The worker is responsible // for unblocking the Process implementation. Interrupt() // Terminate is called when a Worker is removed from the processing pool // and is responsible for cleaning up any held resources. Terminate() } Worker接口定义了Process、BlockUntilReady、Interrupt、Terminate方法closureWorkertype closureWorker struct { processor func(interface{}) interface{} } func (w *closureWorker) Process(payload interface{}) interface{} { return w.processor(payload) } func (w *closureWorker) BlockUntilReady() {} func (w *closureWorker) Interrupt() {} func (w *closureWorker) Terminate() {}closureWorker定义了processor属性,它实现了Worker接口的Process、BlockUntilReady、Interrupt、Terminate方法,其中Process方法委托给processorcallbackWorkertype callbackWorker struct{} func (w *callbackWorker) Process(payload interface{}) interface{} { f, ok := payload.(func()) if !ok { return ErrJobNotFunc } f() return nil } func (w *callbackWorker) BlockUntilReady() {} func (w *callbackWorker) Interrupt() {} func (w *callbackWorker) Terminate() {} callbackWorker定义了processor属性,它实现了Worker接口的Process、BlockUntilReady、Interrupt、Terminate方法,其中Process方法执行的是payload函数Pooltype Pool struct { queuedJobs int64 ctor func() Worker workers []*workerWrapper reqChan chan workRequest workerMut sync.Mutex } func New(n int, ctor func() Worker) *Pool { p := &Pool{ ctor: ctor, reqChan: make(chan workRequest), } p.SetSize(n) return p } func NewFunc(n int, f func(interface{}) interface{}) *Pool { return New(n, func() Worker { return &closureWorker{ processor: f, } }) } func NewCallback(n int) *Pool { return New(n, func() Worker { return &callbackWorker{} }) } Pool定义了queuedJobs、ctor、workers、reqChan、workerMut属性;New方法根据n和ctor创建Pool;NewFunc方法根据n和f来创建closureWorker;NewCallback方法创建callbackWorkerProcessfunc (p *Pool) Process(payload interface{}) interface{} { atomic.AddInt64(&p.queuedJobs, 1) request, open := <-p.reqChan if !open { panic(ErrPoolNotRunning) } request.jobChan <- payload payload, open = <-request.retChan if !open { panic(ErrWorkerClosed) } atomic.AddInt64(&p.queuedJobs, -1) return payload } Process方法首先递增queuedJobs,然后从reqChan读取request,然后往jobChan写入payload,之后再等待retChan,最后递减queuedJobsSetSizefunc (p *Pool) SetSize(n int) { p.workerMut.Lock() defer p.workerMut.Unlock() lWorkers := len(p.workers) if lWorkers == n { return } // Add extra workers if N > len(workers) for i := lWorkers; i < n; i++ { p.workers = append(p.workers, newWorkerWrapper(p.reqChan, p.ctor())) } // Asynchronously stop all workers > N for i := n; i < lWorkers; i++ { p.workers[i].stop() } // Synchronously wait for all workers > N to stop for i := n; i < lWorkers; i++ { p.workers[i].join() } // Remove stopped workers from slice p.workers = p.workers[:n] }SetSize方法首先通过workerMut加锁,然后根据lWorkers创建newWorkerWrapper,之后执行worker.stop,再执行worker.join(),然后清空workersClosefunc (p *Pool) Close() { p.SetSize(0) close(p.reqChan) }Close方法执行SetSize(0)及close(p.reqChan)实例func TestFuncJob(t *testing.T) { pool := NewFunc(10, func(in interface{}) interface{} { intVal := in.(int) return intVal * 2 }) defer pool.Close() for i := 0; i < 10; i++ { ret := pool.Process(10) if exp, act := 20, ret.(int); exp != act { t.Errorf("Wrong result: %v != %v", act, exp) } } }TestFuncJob通过NewFunc创建pool,
-
序本文主要研究一下tunny的workerWrapperworkerWrappertype workerWrapper struct { worker Worker interruptChan chan struct{} // reqChan is NOT owned by this type, it is used to send requests for work. reqChan chan<- workRequest // closeChan can be closed in order to cleanly shutdown this worker. closeChan chan struct{} // closedChan is closed by the run() goroutine when it exits. closedChan chan struct{} } func newWorkerWrapper( reqChan chan<- workRequest, worker Worker, ) *workerWrapper { w := workerWrapper{ worker: worker, interruptChan: make(chan struct{}), reqChan: reqChan, closeChan: make(chan struct{}), closedChan: make(chan struct{}), } go w.run() return &w }workerWrapper包装了worker,定义了interruptChan、reqChan、closeChan、closedChan属性interruptfunc (w *workerWrapper) interrupt() { close(w.interruptChan) w.worker.Interrupt() } interrupt方法关闭w.interruptChan,执行w.worker.Interrupt()runfunc (w *workerWrapper) run() { jobChan, retChan := make(chan interface{}), make(chan interface{}) defer func() { w.worker.Terminate() close(retChan) close(w.closedChan) }() for { // NOTE: Blocking here will prevent the worker from closing down. w.worker.BlockUntilReady() select { case w.reqChan <- workRequest{ jobChan: jobChan, retChan: retChan, interruptFunc: w.interrupt, }: select { case payload := <-jobChan: result := w.worker.Process(payload) select { case retChan <- result: case <-w.interruptChan: w.interruptChan = make(chan struct{}) } case _, _ = <-w.interruptChan: w.interruptChan = make(chan struct{}) } case <-w.closeChan: return } } }run首先创建jobChan、retChan,然后for循环执行select读取reqChan,之后读取jobChan的payload,进行处理,然后写入到retChanstopfunc (w *workerWrapper) stop() { close(w.closeChan) }stop方法关闭w.closeChanjoinfunc (w *workerWrapper) join() { <-w.closedChan } join方法则等待w.closedChan小结tunny的workerWrapper包装了worker,定义了interruptChan、reqChan、closeChan、closedChan属性,它提供了interrupt、run、stop、join方法。doc
-
准备工作日志记录对程序排查问题比较关键,记录下GO中日志选择,从以下出发点考虑:日志文件能自动切割,以免过大能记录从哪个文件哪行代码调用的,方便排查问题配置简单明了库文件使用人数较多,稳定经过一段时间摸索,最终选择了Logrus和lumberjack两个库,使用人数都比较多。安装两个库go get gopkg.in/natefinch/lumberjack.v2 go get github.com/sirupsen/logrus代码实际中一行配置就可以完成,后续只用调用logrus.Debug、logrus.Info同标准库一样只用log改为logrus十分简单明了 logrus.SetOutput(io.MultiWriter(os.Stdout, &lumberjack.Logger{ Filename: "go-log.log", MaxBackups: 10, MaxSize: 20, }))整体代码如下,基本能满足实际使用需求个别需求调整可以参考这两个文档 package main import ( "io" "os" "time" "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" ) func main() { // 同时输出到终端和文件 logrus.SetOutput(io.MultiWriter(os.Stdout, &lumberjack.Logger{ Filename: "go-log.log", MaxBackups: 10, MaxSize: 20, })) /*只输出到文件的话只用以下写法 logrus.SetOutput(&lumberjack.Logger{ Filename: "go-log.log", MaxBackups: 10, MaxSize: 20, }) */ // 这个在日志中记录代码位置,十分有用 logrus.SetReportCaller(true) // 设置输出等级,按实际需求设置,可以忽略 logrus.SetLevel(logrus.InfoLevel) logrus.Debug("debug message") logrus.Infof("info message at %s\n", time.Now().Format("2006-01-02 15:04:05")) logrus.Error("error message") }
-
自从2022年 Golang 1.18 发布至今已有一年多了,在1.18版本中增加了非常重磅的一个功能,那就是泛型!Golang官方也对泛型格外重视:“Generics are the biggest change we’ve made to Go since the first open source release”(泛型是自第一个开源版本以来我们对 Go 所做的最大改变)然而由于平时工作项目所用Go版本较为古老,一直没有大范围应用泛型这个特性,虽然之前刚发布时就了解学习过,但是也有些模糊了,这里还是希望能够记录一下Golang泛型,以备后续参考,我讲基于Golang官方文档、博客、youtube演讲等来进行学习。什么是泛型泛型是一种可以编写独立于使用的特定类型的代码的方法,可以通过编写函数或类型来使用一组类型中的任何一个。泛型为Golang增添了三个重要功能:函数和类型的类型参数将接口类型定义为类型集,包括没有方法的类型。也就是我们可以定义类型集和方法集类型推断,允许函数调用时省略类型参数我们从类型参数开始逐步了解类型参数 Type parameters类型参数让我们可以参数化函数或者具有类型的类型,与普通的参数列表类似,类型参数使用方括号来表示函数中使用类型参数这里有一个常见的取最小值函数,我们经常会在代码中写(新版的Golang官方库已经支持了max以及min):func Min(x, y float64) float64 { if x < y { return x } return y }我们可以通过类型参数来替换 float64 类型来使这个函数更加通用,让这个函数不仅仅适用于 float64 类型,可以这样来做:import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y } m := Min[int](2, 3)在这里我们使用了类型参数 T 来替换 float64 类型使得函数通用,由于 T 是一个新的类型,所以我们要在 [] 中声明它。函数定义好后,与普通的函数调用类似,我们需要传入函数的实参以及创建一个接收值来接受函数实际返回的结果,不同的是,我们需要在 [] 传入具体的类型值,向函数提供类型参数 int 成为实例化。实例化将会分两步进行:编译器将整个泛型函数或类型中的所有类型实参进行替换编译器验证每个类型参数是否满足了各自的约束如果编译器在第二步执行失败,实例化就会失败且程序会fail。我们也可以直接传入类型参数来实例化函数,而不需要传入具体实参进行实际调用,实例化过后我们就可以像普通函数调用一样来调用这个实例化过后的函数了:fmin := Min[float64] m := fmin(2.71, 3.14)类型中使用类型参数前面是一个函数中使用类型参数的例子,还有一个在类型中使用类型参数的例子:type Tree[T interface{}] struct { left, right *Tree[T] value T}func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }var stringTree Tree[string]这是一个通用的二叉树类型定义,我们再次采用了类型参数 T 来作为一个通用性的数据类型,这里定义了类型 Tree[T] 同时定义了它所具有的方法 Lookup(x T) *Tree[T]。最后一行通过 var 对变量 stringTree 做了一次实例化,传入参数类型为 string类型集func min(x,y float64)float64func Gmin[T constraints.Ordered](x, y T) T普通函数 min() 每个参数值都有一个类型,例如min函数中,限定了 x,y 及 返回值只有在 float64 类型时才有效;而函数 Gmin() 类型参数列表中每个类型参数都有一个类型,由于类型参数本身就是一种类型,因此类型参数的类型定义了类型集,这种元类型告诉了我们那些类型对该参数类型有效,因此这个元类型实际定义了类型集,我们可以称之为类型约束。在 Gmin() 中,类型越是是从约束包中导入的,这个包也是 Golang 标准库中新增的包。这个Ordered约束描述了具有可排序值的所有类型的集合,或者换句话说,约束了能够使用 < 运算符(或 <= 、 > 等)进行比较的类型范围。所以只有具有可排序值的类型才能传递给GMin,在GMin函数体中,该类型参数的值可以用于与 < 等运算符进行比较。
-
go整合elasticsearch基于docker搭建开发环境在开发之前我们首先需要借助docker来构建我们的开发环境,先创建一个文件名称为docker-compose.yaml, 里面写入下面的内容:--- version: "3" services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0 container_name: es01 environment: - node.name=es01 - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - discovery.type=single-node ulimits: memlock: soft: -1 hard: -1 volumes: - esdata:/usr/share/elasticsearch/data ports: - 9200:9200 kibana: image: docker.elastic.co/kibana/kibana:7.10.0 ports: - 5601:5601 depends_on: - elasticsearch volumes: esdata: driver: local使用docker-compose up -d 启动容器,之后在浏览器中分别验证es和kibana的运行状态验证es:http://localhost:9200/验证kibana:http://localhost:5601检查客户端apipackage main import ( "fmt" "github.com/elastic/go-elasticsearch/v7" ) func main() { es, err := elasticsearch.NewDefaultClient() if err != nil { fmt.Println(err) return } res, err := es.Info() if err != nil { fmt.Println(err) return } defer res.Body.Close() fmt.Println(res) }索引相关操作创建索引package main import ( "context" "fmt" "github.com/elastic/go-elasticsearch/v7" "log" ) func main() { cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", }, } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } indexName := "test_20230726" res, err := es.Indices.Create( indexName, es.Indices.Create.WithContext(context.Background()), es.Indices.Create.WithPretty()) if err != nil { log.Fatalf("Error creating the index: %s", err) } defer res.Body.Close() fmt.Println(res.String()) }删除索引package main import ( "context" "fmt" "github.com/elastic/go-elasticsearch/v7" "log" ) func main() { cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", }, } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } indexName := "test_20230726" res, err := es.Indices.Delete( []string{indexName}, es.Indices.Delete.WithContext(context.Background()), es.Indices.Delete.WithIgnoreUnavailable(true), es.Indices.Delete.WithPretty(), ) if err != nil { log.Fatalf("Error deleting the index: %s", err) } defer res.Body.Close() fmt.Println(res.String()) }修改索引package main import ( "bytes" "context" "encoding/json" "fmt" "github.com/elastic/go-elasticsearch/v7" "github.com/elastic/go-elasticsearch/v7/esapi" "log" ) func main() { cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", }, } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } indexName := "your_index_name" documentID := "your_document_id" // 准备文档数据 doc := Document{ Title: "Document Title", Body: "This is the body of the document.", } // 将文档数据序列化为JSON字节 data, err := json.Marshal(doc) if err != nil { log.Fatalf("Error marshaling document: %s", err) } // 创建PUT请求 req := esapi.IndexRequest{ Index: indexName, DocumentID: documentID, Body: bytes.NewReader(data), } // 发送PUT请求 res, err := req.Do(context.Background(), es) if err != nil { log.Fatalf("Error indexing document: %s", err) } defer res.Body.Close() fmt.Println(res.String()) }查询索引列表package main import ( "encoding/json" "fmt" "github.com/elastic/go-elasticsearch/v7" "log" ) func main() { cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", }, } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } res, err := es.Indices.Get([]string{"_all"}) if err != nil { log.Fatalf("Error getting indices: %s", err) } defer res.Body.Close() if res.IsError() { log.Fatalf("Error response: %s", res.String()) } var result map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&result); err != nil { log.Fatalf("Error parsing the response body: %s", err) } indices, ok := result["test_20230726"].(map[string]interface{}) if !ok { log.Fatalf("Invalid indices format in the response") } for index := range indices { fmt.Println(index) } }插入文档package main import ( "bytes" "context" "encoding/json" "fmt" "github.com/elastic/go-elasticsearch/v7" "github.com/elastic/go-elasticsearch/v7/esapi" "log" ) type Document struct { Title string `json:"title"` Body string `json:"body"` } func main() { cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", }, } es, err := elasticsearch.NewClient(cfg) if err != nil { log.Fatalf("Error creating the client: %s", err) } indexName := "test_20230726" documentID := "20230726" doc := Document{ Title: "Document Title", Body: "This is the body of the document.", } data, err := json.Marshal(doc) if err != nil { log.Fatalf("Error marshaling document: %s", err) } req := esapi.IndexRequest{ Index: indexName, DocumentID: documentID, Body: bytes.NewReader(data), } res, err := req.Do(context.Background(), es) if err != nil { log.Fatalf("Error indexing document: %s", err) } defer res.Body.Close() fmt.Println(res.String())
-
原理遍历读取/proc/获取所有进程IDcat /proc/5181/stat中前四列分别为进程PID,进程名,进程状态,父进程PIDGo代码1.获取/proc/下面所有文件名+文件夹名为数字的名字2.读取/proc/xxx/stat获取进程信息输出package main import ( "fmt" "io/ioutil" "log" "regexp" "sort" "strconv" ) func main() { var process []int var validId = regexp.MustCompile("^[0-9]+$") infoList, err := ioutil.ReadDir("/proc") if err != nil { log.Println(infoList) } for _, info := range infoList { if info.IsDir() && validId.MatchString(info.Name()) { p, _ := strconv.Atoi(info.Name()) process = append(process, p) } } sort.Ints(process) statRe := regexp.MustCompile(`([0-9]+) \((.+?)\) [a-zA-Z]+ ([0-9]+)`) fmt.Printf("%6s\t%6s\t%s\n", "PID", "PPID", "NAME") for _, p := range process { b, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", p)) if err != nil { continue } matches := statRe.FindStringSubmatch(string(b)) fmt.Printf("%6s\t%6s\t%s\n", matches[1], matches[3], matches[2]) } }package main import ( "fmt" "io/ioutil" "log" "regexp" "sort" "strconv" ) func main() { var process []int var validId = regexp.MustCompile("^[0-9]+$") infoList, err := ioutil.ReadDir("/proc") if err != nil { log.Println(infoList) } for _, info := range infoList { if info.IsDir() && validId.MatchString(info.Name()) { p, _ := strconv.Atoi(info.Name()) process = append(process, p) } } sort.Ints(process) statRe := regexp.MustCompile(`([0-9]+) \((.+?)\) [a-zA-Z]+ ([0-9]+)`) fmt.Printf("%6s\t%6s\t%s\n", "PID", "PPID", "NAME") for _, p := range process { b, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", p)) if err != nil { continue } matches := statRe.FindStringSubmatch(string(b)) fmt.Printf("%6s\t%6s\t%s\n", matches[1], matches[3], matches[2]) } }
-
堆(Heap)堆(Heap),又称为优先队列(Priority Queue)。尽管名为优先队列,但堆并不是队列。在队列中,我们可以进行的操作是向队列中添加元素和按照元素进入队列的顺序取出元素。而在堆中,我们不是按照元素进入队列的先后顺序,而是按照元素的优先级取出元素。问题背景在Linux内核中,调度器根据各个进程的优先级来进行程序的执行调度。在操作系统运行时,通常会有很多个不同的进程,各自优先级也不相同。调度器的作用是让优先级高的进程得到优先执行,而优先级较低的则需要等待。堆是一种适用于实现这种调度器的数据结构。需要提一下,现在Linux内核的调度器使用的是基于红黑树的CFS(Completely Fair Scheduler)。二叉堆的概念我们常用的二叉堆是一颗任意节点的优先级不小于其子节点的完全二叉树。完全二叉树的定义如下:若设二叉树的高度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点,这就是完全二叉树。比如下图就是一颗完全二叉树: 10 / \ 15 30 / \ / \ 40 50 100 40现在假设保存的数值越小的节点的优先级越高,那么上图就是一个堆。我们将任意节点不大于其子节点的堆叫做最小堆或小根堆,将任意节点不小于其子节点的堆叫做最大堆或大根堆。因此,上图就是一个小根堆。优先级队列的实现通过使用Go语言中的container/heap包,我们可以轻松地实现一个优先级队列。这个队列可以用于解决许多问题,如任务调度、事件处理等。通过设置每个项的优先级,我们可以确保在处理队列时按照指定的顺序进行操作。Item通过定义Item结构体来表示优先级队列中的项。每个项具有值(value)和优先级(priority)。index表示项在优先级队列中的索引。// Item represents an item in the priority queue. type Item struct { value int // 项的值。 priority int // 项的优先级。 index int // 项在队列中的索引。 }PriorityQueuePriorityQueue是一个切片类型,实现了heap.Interface接口。它提供了用于操作优先级队列的方法,如插入、删除和修改。Len方法返回优先级队列的长度。Less方法比较两个项的优先级。Swap方法交换两个项在优先级队列中的位置。Push方法向优先级队列中添加一个项。Pop方法移除并返回优先级队列中的最小项。Update方法用于修改项的优先级并更新其在优先级队列中的位置。// PriorityQueue 实现了 heap.Interface 接口。 type PriorityQueue []*Item // Len 返回优先级队列的长度。 func (pq PriorityQueue) Len() int { return len(pq) } // Less 比较优先级队列中的两个项。 func (pq PriorityQueue) Less(i, o int) bool { return pq[i].priority < pq[o].priority } // Swap 交换优先级队列中的两个项。 func (pq PriorityQueue) Swap(i, o int) { pq[i], pq[o] = pq[o], pq[i] pq[i].index = i pq[o].index = o} // Push 向优先级队列中添加一个项。 func (pq *PriorityQueue) Push(x interface{}) { item := x.(*Item) item.index = len(*pq) *pq = append(*pq, item) } // Pop 移除并返回优先级队列中的最小项。 func (pq *PriorityQueue) Pop() interface{} { old := *pq n := len(old) item := old[n-1] old[n-1] = nil // 避免内存泄漏 item.index = -1 *pq = old[0 : n-1] return item } // Update 修改项的优先级并更新其在优先级队列中的位置。 func (pq *PriorityQueue) Update(item *Item, value, priority int) { item.value = value item.priority = priority heap.Fix(pq, item.index) }改进但是我们经常有一种场景,需要堆的快速求最值的性质,又需要能够支持快速的访问元素,特别是删除元素。 如果我们要查找堆中的某个元素,需要遍历一遍。非常麻烦。比如延迟任务的场景,我们可以使用堆对任务的到期时间戳进行排序,从而实现到期任务自动执行,但是它没办法支持删除一个延迟任务的需求。HeapMap一种能够快速随机访问元素的数据结构是哈希表。使用哈希表实现的map可以在O(1)的时间复杂度下进行随机访问。另外,堆结构可以在O(log(n))的时间复杂度下删除元素,前提是知道要删除的元素的下标。因此,我们可以将这两个数据结构结合起来使用。使用哈希表记录堆中每个元素的下标,同时使用堆来获取最值元素。在PriorityQueue中定义一个dataMapdataMap是一个用于存储队列中的项的映射表,它的好处是可以根据项的键快速地查找到对应的项。 在PriorityQueue中,有一个数据切片data,用于存储队列中的项,并且用一个索引值index来表示项在切片中的位置。dataMap则以项的键作为索引,将项的指针映射到该键上。使用dataMap的好处是可以快速地根据键找到对应的项,而不需要遍历整个切片。这对于需要频繁查找和修改项的场景非常重要,可以提高代码的效率。如果没有dataMap,想要根据键找到对应的项则需要遍历整个切片进行查找,时间复杂度将为O(n)。而使用dataMap可以将查找的时间复杂度降低到O(1),提高代码的性能。另外,需要注意的是dataMap必须与data切片保持同步,即在对切片进行修改时,需要同时更新dataMap,保持两者的一致性。否则,在使用dataMap时会出现不一致的情况,导致错误的结果。因此,在使用PriorityQueue时,需要确保维护dataMap和data切片的一致性。push在Push时需要保证Key值唯一func (pq *PriorityQueue) Push(i *Item) error { if i == nil || i.Key == "" { return errors.New("error adding item: Item Key is required") } pq.lock.Lock() defer pq.lock.Unlock() if _, ok := pq.dataMap[i.Key]; ok { return ErrDuplicateItem } // Copy the item value(s) so that modifications to the source item does not // affect the item on the queue clone, err := copystructure.Copy(i) if err != nil { return err } pq.dataMap[i.Key] = clone.(*Item) heap.Push(&pq.data, clone) return nil }popPopByKey方法可以根据Key查找并移除对应的元素// PopByKey searches the queue for an item with the given key and removes it // from the queue if found. Returns nil if not found. This method must fix the // queue after removing any key. func (pq *PriorityQueue) PopByKey(key string) (*Item, error) { pq.lock.Lock() defer pq.lock.Unlock() item, ok := pq.dataMap[key] if !ok { return nil, nil } // Remove the item the heap and delete it from the dataMap itemRaw := heap.Remove(&pq.data, item.index) delete(pq.dataMap, key) if itemRaw != nil { if i, ok := itemRaw.(*Item); ok { return i, nil } } return nil, nil }
-
基于终端库termbox-go做了个贪吃蛇游戏, 功能较简单,代码约160行左右一:原理介绍1. 绘制原理存储好蛇身和食物坐标都存储在Snake结构中定时300毫秒执行移动蛇身/生成食物,然后清空终端再重新根据坐标绘制点●达到模拟动画效果type Location struct { X int Y int } type Snake struct { Body []Location Food Location ...... } func Draw(s *Snake) { termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) for _, location := range s.Body { termbox.SetCell(location.X, location.Y, '●', termbox.ColorGreen, termbox.ColorDefault) } termbox.SetCell(s.Food.X, s.Food.Y, '●', termbox.ColorRed, termbox.ColorDefault) termbox.Flush() }2.贪吃蛇移动过程原理很简单,根据当前行走方向,追加一个点到[]Localtion如果蛇头位置不是食物位置,删除[]Localtion第一个点, 添加一个点(最后一个点位置+1)相当于行走了如果恰好是食物位置,添加一个点(最后一个点位置+1),再随机生成食物// 移动一步, 如果碰壁返回false, 否则返回true func (s *Snake) Move() bool { head := s.GetHead() switch s.Direction { case DIRECTION_UP: s.Body = append(s.Body, Location{head.X, head.Y - 1}) case DIRECTION_DOWN: s.Body = append(s.Body, Location{head.X, head.Y + 1}) case DIRECTION_LEFT: s.Body = append(s.Body, Location{head.X - 1, head.Y}) case DIRECTION_RIGHT: s.Body = append(s.Body, Location{head.X + 1, head.Y}) } head = s.GetHead() // 蛇头到达食物位置时标记食物已吃,并且追加到蛇尾(s.Body[0]不用剔除, 否则剔除) if head == s.Food { s.FoodEated = true s.RandomFood() s.Score += 10 } else { s.Body = s.Body[1:] } return 0 <= head.X && head.X <= s.MaxX && 0 <= head.Y && head.Y <= s.MaxY }3.生成食物过程仅需要注意是否生成在蛇身本身,是的话再生成// 判断生成的食物坐标是否在蛇身上 func (s *Snake) isFoodInSnake(location Location) bool { for _, l := range s.Body { if l == location { return true } } return false } // 生成食物 func (s *Snake) RandomFood() { w, h := termbox.Size() // 上下两边留点空隙 location := Location{rand.Intn(w-10) + 5, rand.Intn(h-10) + 5} for s.isFoodInSnake(location) { location = Location{rand.Intn(w), rand.Intn(h)} } s.Food = location }
-
要用Go获取短信验证码,通常需要连接到一个短信服务提供商的API,并通过该API发送请求来获取验证码。由于不同的短信服务提供商可能具有不同的API和授权方式,我将以一个简单的示例介绍如何使用Go语言来获取短信验证码。在这个示例中,我们将使用中昱维信作为短信服务提供商。1.注册账号并获取API密钥注册并登录你的短信平台,然后获取AppID和AppKey,注册地址在代码里2.创建验证码模版创建验证码模版,获取验证码模版id3.使用Go代码调用短信服务接口// 平台注册地址 vip.veesing.com package main import ( "fmt" "strings" "net/http" "io/ioutil" ) func main() { url := "https://vip.veesing.com/smsApi/verifyCode" method := "POST" // 替换示例代码中的"YOUR_APP_ID"、"YOUR_APP_KEY"、"YOUR_TEMPLATE_ID"、"YOUR_PHONE"、"YOUR_CODE"为你在中昱维信账号中获得的实际值 payload := strings.NewReader("appId=YOUR_APP_ID&appKey=YOUR_APP_KEY&templateId=YOUR_TEMPLATE_ID&phone=YOUR_PHONE&variables=YOUR_CODE") client := &http.Client { } req, err := http.NewRequest(method, url, payload) if err != nil { fmt.Println(err) return } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") res, err := client.Do(req) if err != nil { fmt.Println(err) return } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println(err) return } // 解析短信服务的响应response,根据返回结果判断是否发送成功 // 成功 {"returnStatus": "1 ", "message": "成功", "remainPoint": "241", "taskId": "3313746", "successCounts": "1"} // 失败 {"returnStatus": "0", "message": "参数错误", "remainPoint": null, "taskId": null, "successCounts": null} // 处理成功或失败的逻辑... fmt.Println(string(body)) }
-
在go的sync包中,有一个singleflight包,里面有一个 singleflight.go文件,代码加注释,一共200行出头,通过 singleflight可以很容易实现缓存和去重的效果,避免重复计算,接下来我们就给大家详细介绍一下sync.singleflight如何解决热点缓存穿透问题在 go 的 sync 包中,有一个 singleflight 包,里面有一个 singleflight.go 文件,代码加注释,一共 200 行出头。内容包括以下几块儿:Group 结构体管理一组相关的函数调用工作,它包含一个互斥锁和一个 map,map 的 key 是函数的名称,value 是对应的 call 结构体。call 结构体表示一个 inflight 或已完成的函数调用,包含等待组件 WaitGroup、调用结果 val 和 err、调用次数 dups 和通知通道 chans。Do 方法接收一个 key 和函数 fn,它会先查看 map 中是否已经有这个 key 的调用在 inflight,如果有则等待并返回已有结果,如果没有则新建一个 call 并执行函数调用。DoChan 类似 Do 但返回一个 channel 来接收结果。doCall 方法包含了具体处理调用的逻辑,它会在函数调用前后添加 defer 来 recover panic 和区分正常 return 与 runtime.Goexit。如果发生 panic,会将 panicwraps 成错误返回给等待的 channel,如果是 goexit 会直接退出。正常 return 时会将结果发送到所有通知 channel。Forget 方法可以忘记一个 key 的调用,下次 Do 时会重新执行函数。这个包通过互斥锁和 map 实现了对相同 key 的函数调用去重,可以避免对已有调用的重复计算,同时通过 channel 机制可以通知调用者函数执行结果。在一些需要确保单次执行的场景中,可以使用这个包中的方法。通过 singleflight 可以很容易实现缓存和去重的效果,避免重复计算,接下来,我们来模拟一下并发请求可能导致的缓存穿透场景,以及如何用 singleflight 包来解决这个问题:package main import ( "context" "fmt" "golang.org/x/sync/singleflight" "sync/atomic" "time" ) type Result string // 模拟查询数据库 func find(ctx context.Context, query string) (Result, error) { return Result(fmt.Sprintf("result for %q", query)), nil } func main() { var g singleflight.Group const n = 200 waited := int32(n) done := make(chan struct{}) key := "this is key" for i := 0; i < n; i++ { go func(j int) { v, _, shared := g.Do(key, func() (interface{}, error) { ret, err := find(context.Background(), key) return ret, err }) if atomic.AddInt32(&waited, -1) == 0 { close(done) } fmt.Printf("index: %d, val: %v, shared: %v\n", j, v, shared) }(i) } select { case <-done: case <-time.After(time.Second): fmt.Println("Do hangs") } time.Sleep(time.Second * 4) }在这段程序中,如果重复使用查询结果,shared 会返回 true,穿透查询会返回 false上面的设计中还有一个问题,就是在 Do 阻塞时,所有请求都会阻塞,内存可能会出现大的问题。此时,Do 可以更换为DoChan,两者实现上完全一样,不同的是,DoChan() 通过 channel 返回结果。因此可以使用 select 语句实现超时控制ch := g.DoChan(key, func() (interface{}, error) { ret, err := find(context.Background(), key) return ret, err }) // Create our timeout timeout := time.After(500 * time.Millisecond) var ret singleflight.Result select { case <-timeout: // Timeout elapsed fmt.Println("Timeout") return case ret = <-ch: // Received result from channel fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared) }在超时时主动返回,不阻塞。此时又引入了另一个问题,这样的每一次的请求,并不是高可用的,成功率是无法保证的。这时候可以增加一定的请求饱和度来保证业务的最终成功率,此时一次请求还是多次请求,对于下游服务而言并没有太大区别,此时使用 singleflight 只是为了降低请求的数量级,那么可以使用 Forget() 来提高下游请求的并发。ch := g.DoChan(key, func() (interface{}, error) { go func() { time.Sleep(10 * time.Millisecond) fmt.Printf("Deleting key: %v\n", key) g.Forget(key) }() ret, err := find(context.Background(), key) return ret, err })当然,这种做法依然无法保证100%的成功,如果单次的失败无法容忍,在高并发的场景下需要使用更好的处理方案,比如牺牲一部分实时性、完全使用缓存查询 + 异步更新等。
-
golang 项目 引用 托管的代码库,导入代码:提示错误信息:codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec: cannot find module providing package codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec: unrecognized import path "codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec": reading https://codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec?go-get=1: 404 Not Found
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签