golang 闭包函数的应用技巧

一、有名函数和匿名函数

函数变量类型初始值为nil。函数字面量类型的语法表达格式是 func (InputTypeList) OutputTypeList

//无参函数
func fun() {
    
       
}
var f func()//无入参无返回值的函数对象声明,初始值为nil
f = fun
//有参函数
type FT func(int)
func Fa(int){
    
    }
func Test(FT){
    
    }
Test(Fa) //pass function as parameter

“有名函数”和“匿名函数(没有函数名类似闭包)”的类型都属于函数字面量类型,有名函数的定义相当于初始化一个函数字面量类型后将其赋值给一个函数名变量,“匿名函数”的定义也是直接初始化一个函数字面量类型,只是没有绑定到一个具体函数名变量上。从 Go 类型系统的角度来看,“有名函数”和“匿名函数”都是函数字面量类型的实例。可以使用 type NewType OldType 语法定义一种新类型,这种类型都是命名类型,同理可以使用该方法定义一种新类型——函数命名类型,简称函数类型,例如:type NewFuncType FuncLiteral 依据Go语言类型系统的概念,NewFuncType 为新定义的函数命名类型,FuncLiteral 为函数字面量类型,FuncLiteral 为函数类型 NewFuneType 的底层类型。

type CalculateType func(int, int) // 声明了一个函数类型
// 该函数类型实现了一个方法
func (c *CalculateType) Serve() {
    
    
  fmt.Println("我是一个函数类型")
}
// 加法函数
func add(a, b int) {
    
    
  fmt.Println(a + b)
}
// 乘法函数
func mul(a, b int) {
    
    
  fmt.Println(a * b)
}
func main() {
    
    
  a := CalculateType(add) // 将add函数强制转换成CalculateType类型
  b := CalculateType(mul) // 将mul函数强制转换成CalculateType类型
  a(2, 3)
  b(2, 3)
  a.Serve()
  b.Serve()
}

二、方法作为函数变量传递

当特定对象实例的方法method作为函数指针时传递时,接受者会保证在调用的时候,调用到是这个对象实例的method,method的任何操作都会针对该对象实例生效,而且不需要传任何类似于this、self指针之类的东西,换句话说,对象实例+method 作为绑定的整体传递给接受者的

type Outer struct{
    
    
    a string
}
func (o *Outer)Hello(in *Inner){
    
    
   fmt.Println("Inner:",in,"\tcall\tOuter:",o.a)
}
type Inner struct{
    
    
        a string
    cb Cb
}
type Cb func(*Inner)
func (in *Inner)Register(cb Cb){
    
    
    in.cb = cb
}
func (in *Inner)Say(){
    
    
    in.cb(in)
}

func main() {
    
    
    out1 := Outer{
    
    a:"out1 instance"}
    out2 := Outer{
    
    a:"out2 instance"}
    fmt.Println("out1 :",&out1)
    fmt.Println("out2 :",&out2) 
    in1 := Inner{
    
    a:"int1 instance"}
    in1.Register(out1.Hello)
    in1.Say()
    in1.Register(out2.Hello)
    in1.Say()
}

三、闭包构成的三种情况

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。

  • 闭包里没有引用环境(变量生命周期很短,调用完即释放)

    // 第一种场景
    func fib01() func() int {
    	return func() int {
    		a, b := 0, 1
    		a, b = b, a+b
    		return a
    	}
    }
    
  • 闭包里引用全局变量(变量生命周期就是全局变量生命周期)

    var y int
    // 第二种场景
    func fib00() func() int {
          
          
    	return func() int {
          
          
    		y++
    		return y
    	}
    }
    
  • 闭包里引用局部变量(变量生命周期长,调用完不释放,下次调用会继续引用,相当于变相延长了函数的生命周期),闭包可能会导致变量逃逸到堆上来延长变量的生命周期,给 GC 带来压力。

    func AntherExFunc(n int) func() {
          
          
        n++
        return func() {
          
          
            fmt.Println(n)
        }
    }
    
    func ExFunc(n int) func() {
          
          
        return func() {
          
          
            n++
            fmt.Println(n)
        }
    }
    
    func main() {
          
          
        myAnotherFunc:=AntherExFunc(20)
        fmt.Println(myAnotherFunc)  //0x48e3d0  在这儿已经定义了n=20 ,然后执行++ 操作,所以是21 。
        myAnotherFunc()     //21 后面对闭包的调用,没有对n执行加一操作,所以一直是21
        myAnotherFunc()     //21
        myFunc:=ExFunc(10)
        fmt.Println(myFunc)  //0x48e340   这儿定义了n 为10
        myFunc()       //11  后面对闭包的调用,每次都对n进行加1操作。
        myFunc()       //12
    }
    

四、for循环中的并发闭包:

//因为for语句里面中闭包使用的v是外部的v变量,当执行完循环之后,v最终是c,所以如果在主协程执行完for之后,定义的子协程才开始执行结果可能是ccc,
//如果for过程中,子协程先执行了,结果就可能不是c, c,c”。 
func test1() {
    
                    
    s := []string{
    
    "a", "b", "c"}                             
    for _, v := range s {
    
     
        go func() {
    
    
            fmt.Println(v)
        }()                 
    }                        
    time.Sleep(time.Second * 1)                                                       
}
//for程序如果想输出a,b,c的解决方法:
//只需要每次将变量v的拷贝传进函数即可,但此时就不是使用的上下文环境中的变量了。
func test2() {
    
                    
    s := []string{
    
    "a", "b", "c"}                             
    for _, v := range s {
    
     
        go func(v string) {
    
    
            fmt.Println(v)
        }(v)   //每次将变量 v 的拷贝传进函数                 
    }                        
    select {
    
    }                                                      
}

//结果为4 4 4 4 4,函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4
func test1() {
    
    
    var users [5]struct{
    
    }
    for i := range users {
    
    
        defer func() {
    
     fmt.Println(i) }()
    }
}
//defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份利用函数参数拷贝,输出为 4 3 2 1 0
func test1() {
    
    
    var users [5]struct{
    
    }
    for i := range users {
    
    
        defer Print(i)
    }
}
func Print(i int) {
    
    
    fmt.Println(i)
}

select语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select会随机地选取其中之一执行。select {}会一直阻塞。很多时候我们需要让main函数不退出,利用select {}让它在后台一直执行

func main() {
    
    
    for i := 0; i < 20; i++ {
    
     //启动20个协程处理消息队列中的消息
        c := consumer.New()
        go c.Start()
    }
    select {
    
    } // 阻塞
}

五、闭包的应用场景和作用:

1、延迟调用,关键字 defer 用于注册延迟调用。defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用 。defer 中使用匿名函数依然是一个闭包。

func main() {
    
    
    x, y := 1, 2
    defer func(a int) {
    
     
        fmt.Printf("x:%d,y:%d\n", a, y)  // y 为闭包引用
    }(x)      // 复制 x 的值
    x += 100
    y += 100
    fmt.Println(x, y)
}

2、变量空间隔离,避免变量污染外部无法对闭包引用的上下文变量进行直接操作,保证了私有性。

// 函数计数器  利用闭包每个计数器有自己独立未暴露的sum
func counter(f func()) func() int {
    
      
	sum := 0
	return func() int {
    
    
		f()
		sum += 1
		return sum
	}
}

// 测试的调用函数
func foo() {
    
    
	fmt.Println("call foo")
}

func main() {
    
    
	cnt := counter(foo)
	cnt()
	cnt()
	cnt()
	fmt.Println(cnt())
}
/*
输出结果:
call foo
call foo
call foo
call foo
4
*/
---
//加法器
func adder() func(int) int {
    
    
	sum := 0
	return func(x int) int {
    
    
		sum += x
		return sum
	}
}

func main() {
    
    
	myAdder := adder()
	// 从1加到10
	for i := 1; i <= 10; i++ {
    
    
		myAdder(i)
	}
	fmt.Println(myAdder(0))
	// 再加上45
	fmt.Println(myAdder(45))
}
---

//斐波那契闭包处理
func fibonacci() func() int {
    
    
	b0 := 0
	b1 := 1
	return func() int {
    
    
		tmp := b0 + b1
		b0 = b1
		b1 = tmp
		return b1
	}

}

func main() {
    
    
	myFibonacci := fibonacci()
	for i := 1; i <= 5; i++ {
    
    
		fmt.Println(myFibonacci())
	}
}

3、装饰函数:函数是Go语言的一等公民,可以作为函数的参数进行传递。装饰器指的就是函数作为参数进行传递的情况。用于构造函数中对对象的封装和处理(尤其是成员较多的结构体)。闭包函数中本身是没有定义变量,而是引用了它所在的环境中的变量。

// Options 调用参数  被装饰的结构体
type Options struct {
    
    
  ...
  Discovery            discovery.Discovery
  LoadBalance          loadbalance.LoadBalancer
  CircuitBreaker       circuitbreaker.CircuitBreaker
}

// Option 调用参数工具函数
type Option func(*Options)


// WithDiscovery 指定服务发现
func WithDiscovery(d discovery.Discovery) Option {
    
    

  return func(o *Options) {
    
    
  	o.Discovery = d
  }
}
...//其它类似的装饰函数省略。。。。

// selector默认实现,内部自动串好 服务发现 负载均衡 熔断隔离 等流程
type Selector struct{
    
    }
// Select 输入service name,返回一个可用的node
func (s *Selector) Select(serviceName string, opt ...Option) (*registry.Node, error) {
    
    
  ...
  opts := &Options{
    
    
  	Discovery:      discovery.DefaultDiscovery,
  	LoadBalance:    loadbalance.DefaultLoadBalancer,
  	CircuitBreaker: circuitbreaker.DefaultCircuitBreaker,
  }
  for _, o := range opt {
    
    
  	o(opts)
  }
  ....
}

//newSelector
func newSelector(registry  discovery.DefaultDiscovery)*registry.Node {
    
    
  selector := &Selector{
    
    }
  n, err := selector.Select("configServer",WithDiscovery(registry ))
  ....
  return n
}

4、闭包引用的上下文变量会延长生命期,避免再次传递。

//普通函数:传递根据哪个后缀判断,其次是文件名字
func makeSuffix (suffix string, name string) string {
    
    
    if !strings.HasSuffix(name, suffix) {
    
    
        return name + suffix  //如果没有后缀就拼接
    }
    return name

} 
func main(){
    
    
    fmt.Println("文件名处理后:", makeSuffix("jpg","go语言圣经"))  
    fmt.Println("文件名处理后:", makeSuffix("jpg","PHP设计模式.jpg"))
}
---
//闭包函数,生成函数可以传入一个文件名,如果该文件名没有指定的后缀(如.jpg),则返回.jpg,如果有则全称
func makeSuffix (suffix string) func (string) string {
    
    
    return func (name string) string {
    
    
        if !strings.HasSuffix(name, suffix) {
    
    
            return name + suffix  //如果没有后缀就拼接
        }
        return name
    }
} 
func main(){
    
    
    //先返回一个闭包
    test := makeSuffix(".jpg")
    fmt.Println("文件名处理后:", test("go语言圣经"))  
    fmt.Println("文件名处理后:", test("PHP设计模式.jpg"))

}

5、内嵌匿名函数的并发调用,原因代码逻辑有for循环或处理时间较长的逻辑需要匿名函数包住另起协程运行,从而不阻塞影响后续逻辑。或者有defer等调用次序的问题需要在匿名函数中先调用defer等。

func main() {
    
    
    ch := make(chan struct{
    
    })
    go func() {
    
    
        fmt.Println("do something..")
        for {
    
    
        	 // process
		}
        time.Sleep(time.Second * 1)
        ch <- struct{
    
    }{
    
    }
    }()

    <-ch
    fmt.Println("I am finished")
}

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/112974589