目錄
1、什麼是閉包?
1.2.1 函數式程式設計
1.2.2 函數作用域
1.2.3 作用域的繼承關係
#1.3.1 初看閉包
1.3.2 閉包中的指標和值
1.3.2 閉包延遲化
2、闭包的好处与坏处?
3、闭包怎么实现的?
4、淺聊一下
首頁 後端開發 Golang 一文淺析Golang中的閉包

一文淺析Golang中的閉包

Nov 21, 2022 pm 08:36 PM
go 閉包 後端

一文淺析Golang中的閉包

1、什麼是閉包?

在真正講述閉包之前,我們先鋪墊一點知識點:

  • 函數式程式設計
  • 函數作用域
  • 作用域的繼承關係

【相關推薦:Go影片教學

1.1 前提知識鋪墊

1.2.1 函數式程式設計

函數式程式設計是一種程式設計範式,看待問題的一種方式,每一個函數都是為了用小函數組織成為更大的函數,函數的參數也是函數,函數回傳的也是函數。我們常見的程式設計範式有:

  • 命令式程式設計:
    • 主要想法為:專注於電腦執行的步驟,也就是一步一步告訴電腦先做什麼再做什麼。
    • 先把解決問題步驟規範化,抽象化為某種演算法,然後再寫具體的演算法去實現,一般只要支援過程化程式設計範式的語言,我們都可以稱為過程化程式語言,例如BASIC, C 等。
  • 聲明式程式設計:
    • 主要想法為:告訴電腦應該做什麼,但是不指定具體要怎麼做,例如 SQL,網頁程式設計的 HTML,CSS。
  • 函數式程式設計:
    • 只專注於做什麼而不關注怎麼做,有一絲宣告式程式設計的影子,但是更專注於」函數是第一位「的原則,也就是函數可以出現在任何地方,參數、變數、回傳值等等。

函數式程式設計可以認為是物件導向程式設計的對立面,一般只有一些程式語言會強調一種特定的程式設計方式,大多數的語言都是多範式語言,可以支援多種不同的程式設計方式,例如JavaScript ,Go 等。

函數式程式設計是一種思考方式,將電腦運算視為函數的計算,是一種寫程式碼的方法論,其實我應該聊函數式程式設計,然後再聊到閉包,因為閉包本身就是函數式程式設計裡面的一個特點之一。

在函數式程式設計中,函數是頭等物件,意思是說一個函數,既可以作為其它函數的輸入參數值,也可以從函數中傳回值,被修改或被指派給一個變數。 (維基百科)

一般純函數程式語言是不允許直接使用程式狀態以及可變物件的,函數式程式設計本身就是要避免使用共享狀態可變狀態,盡可能避免產生副作用

函數式程式設計一般有以下特點:

  • 函數是第一等公民:函數的地位放在第一位,可以當作參數,可以賦值,可以傳遞,可以當做回傳值。

  • 沒有副作用:函數要保持純粹獨立,不能修改外部變數的值,不修改外部狀態。

  • 引用透明:函數運行不依賴外部變數或狀態,相同的輸入參數,任何情況,所得到的回傳值都應該是一樣的。

1.2.2 函數作用域

#作用域(scope),程式設計概念,通常來說,一段程式碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼範圍就是這個名字的作用域

簡單易懂的說,函數作用域是指函數可以運作的範圍。函數有點像盒子,一層套一層,作用域我們可以理解為是個封閉的盒子,也就是函數的局部變量,只能在盒子內部使用,成為獨立作用域。

一文淺析Golang中的閉包

函數內的局部變數,出了函數就跳出了作用域,找不到該變數。 (裡層函數可以使用外層函數的局部變量,因為外層函數的作用域包括了裡層函數),例如下面的innerTmep 出了函數作用域就找不到該變量,但是outerTemp 在內層函數裡面還是可以使用。

一文淺析Golang中的閉包

不管是任何語言,基本上存在一定的記憶體回收機制,也就是回收用不到的記憶體空間,回收的機制一般和上面說的函數的作用域是相關的,局部變數出了其作用域,就有可能被回收,如果還被引用著,那麼就不會被回收。

1.2.3 作用域的繼承關係

所謂作用域繼承,就是前面說的小盒子可以繼承外層大盒子的作用域,在小盒子可以直接取出大盒子的東西,但是大盒子不能取出小盒子的東西,除非發生了逃逸(逃逸可以理解為小盒子的東西給出了引用​​,大盒子拿到就可以使用)。一般而言,變數的作用域有以下兩種:

  • 全域作用域:作用於任何地方

  • ## 局部作用域:一般是程式碼區塊,函數、套件內,

    函數內部宣告/定義的變數叫做局部變數作用域僅限於函數內部

1.2 閉包的定義

「多數情況下我們並不是先理解後定義,而是先定義後理解“,先下定義,

讀不懂沒關係

閉包(closure)是

一個函數以及其捆綁的周邊環境狀態(lexical environment,詞法環境)的引用的組合。換而言之,閉包讓開發者可以從內部函數存取外部函數的作用域。閉包會隨著函數的建立而同時建立。

一句話表達:

#=函數 引用環境閉包= 函數引用環境

以上定義找不到Go語言這幾個字眼,聰明的同學一定知道,閉包是和語言無關的,不是JavaScript 特有的,也不是Go 特有的,而是函數式程式語言的獨特的,是的,你沒有看錯,任何支援函數式程式設計的語言都支援閉包,Go 和JavaScript 就是其中之二, 目前Java 目前版本也是支援閉包的,但有些人可能認為不是完美的閉包,詳細情況文中討論。

1.3 閉包的寫法

#1.3.1 初看閉包

下面是一段閉包的程式碼:

import "fmt"

func main() {
	sumFunc := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
	fmt.Println("先获取函数,不求结果")
	var sum = func() int {
		fmt.Println("求结果...")
		result := 0
		for _, v := range arr {
			result = result + v
		}
		return result
	}
	return sum
}
登入後複製

輸出的結果:

先获取函数,不求结果
等待一会
求结果...
结果: 15
登入後複製

可以看出,裡面的sum() 方法可以引用外部函數lazySum() 的參數以及局部變量,在lazySum()返回函數sum() 的時候,相關的參數和變數都保存在傳回的函數中,可以之後再進行調用。

上面的函數或許還可以更進一步,體現出捆綁函數和周圍的狀態,我們加上一個次數count

import "fmt"
func main() {
	sumFunc := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())
	fmt.Println("结果:", sumFunc())
	fmt.Println("结果:", sumFunc())
}func lazySum(arr []int) func() int {
	fmt.Println("先获取函数,不求结果")
	count := 0
	var sum = func() int {
		count++
		fmt.Println("第", count, "次求结果...")
		result := 0
		for _, v := range arr {
			result = result + v
		}		return result
	}	return sum
}
登入後複製

上面程式碼輸出什麼呢?次數count 會不會發生變化,count明顯是外層函數的局部變量,但是在記憶體函數引用(捆綁),內層函數被暴露出去了,執行結果如下:

先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15
登入後複製

結果是count 其實每次都會變化,這種情況總結一下:

  • 函數體內嵌套了另外一個函數,並且傳回值是一個函數。
  • 內層函數被揭露出去,被外層函數以外的地方引用著,形成了閉包。

此時有人可能有疑問了,前面是lazySum()被創建了1 次,執行了3 次,但是如果是3 次執行都是不同的創建,會是怎麼樣呢?實驗一下:

import "fmt"
func main() {
	sumFunc := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())

	sumFunc1 := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc1())

	sumFunc2 := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc2())
}func lazySum(arr []int) func() int {
	fmt.Println("先获取函数,不求结果")
	count := 0
	var sum = func() int {
		count++
		fmt.Println("第", count, "次求结果...")
		result := 0
		for _, v := range arr {
			result = result + v
		}		return result
	}	return sum
}
登入後複製

執行的結果如下,每次執行都是第1 次:

先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
登入後複製

從以上的執行結果可以看出:

閉套件被創建的時候,引用的外部變數count就已經被創建了1 份,也就是各自呼叫是沒有關係的

繼續拋出一個問題,**如果一個函數回傳了兩個函數,這是一個閉包還是兩個閉包呢? **下面我們實作一下:

一次傳回兩個函數,一個用來計算加和的結果,一個計算乘積:

import "fmt"
func main() {
	sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())
	fmt.Println("结果:", productSFunc())
}func lazyCalculate(arr []int) (func() int, func() int) {
	fmt.Println("先获取函数,不求结果")
	count := 0
	var sum = func() int {
		count++
		fmt.Println("第", count, "次求加和...")
		result := 0
		for _, v := range arr {
			result = result + v
		}		return result
	}	var product = func() int {
		count++
		fmt.Println("第", count, "次求乘积...")
		result := 0
		for _, v := range arr {
			result = result * v
		}		return result
	}	return sum, product
}
登入後複製

運行結果如下:

先获取函数,不求结果
等待一会
第 1 次求加和...
结果: 15
第 2 次求乘积...
结果: 0
登入後複製

從上面結果可以看出,閉包是函數返回函數的時候,不管多少個返回值(函數),都是一次閉包,如果返回的函數有使用外部函數變量,則會綁定到一起,相互影響:

一文淺析Golang中的閉包

閉包綁定了周圍的狀態,我理解此時的函數就擁有了狀態,讓函數具有了物件所有的能力,函數具有了狀態。

1.3.2 閉包中的指標和值

上面的例子,我們閉包中用到的都是數值,如果我們傳遞指針,會是怎麼樣的呢?

import "fmt"
func main() {
	i := 0
	testFunc := test(&i)
	testFunc()
	fmt.Printf("outer i = %d\n", i)
}func test(i *int) func() {
	*i = *i + 1
	fmt.Printf("test inner i = %d\n", *i)	return func() {
		*i = *i + 1
		fmt.Printf("func inner i = %d\n", *i)
	}
}
登入後複製

運行結果如下:

test inner i = 1
func inner i = 2
outer i = 2
登入後複製

可以看出如果是指標的話,閉包裡面修改了指針對應的位址的值,也會影響閉包外面的值。這個其實很容易理解,Go 裡面沒有引用傳遞,只有值傳遞,那我們傳遞指標的時候,也是值傳遞,這裡的值是指標的數值(可以理解為位址值)。

當我們函數的參數是指標的時候,參數會拷貝一份這個指標位址,當做參數進行傳遞,因為本質還是位址,所以內部修改的時候,仍然可以對外部產生影響。

閉包裡面的數據其實位址也是一樣的,下面的實驗可以證明:

func main() {
	i := 0
	testFunc := test(&i)
	testFunc()
	fmt.Printf("outer i address %v\n", &i)
}
func test(i *int) func() {
	*i = *i + 1
	fmt.Printf("test inner i address %v\n", i)
	return func() {
		*i = *i + 1
		fmt.Printf("func inner i address %v\n", i)
	}
}
登入後複製

輸出如下, 因此可以推斷出,閉包如果引用外部環境的指標數據,只是會拷貝一份指標位址數據,而不是拷貝一份真正的數據(==先留個問題:拷貝的時機是什麼時候呢==):

test inner i address 0xc0003fab98
func inner i address 0xc0003fab98
outer i address 0xc0003fab98
登入後複製

1.3.2 閉包延遲化

上面的例子彷彿都在告訴我們,閉包創建的時候,資料就已經拷貝了,但真的是這樣麼?

下面是繼續前面的實驗:

func main() {
	i := 0
	testFunc := test(&i)
	i = i + 100
	fmt.Printf("outer i before testFunc  %d\n", i)
	testFunc()
	fmt.Printf("outer i after testFunc %d\n", i)
}func test(i *int) func() {
	*i = *i + 1
	fmt.Printf("test inner i = %d\n", *i)
		return func() {
		*i = *i + 1
		fmt.Printf("func inner i = %d\n", *i)
	}
}
登入後複製

我們在創建閉包之後,把數據改了,之後執行閉包,答案肯定是真實影響閉包的執行,因為它們都是指針,都是指向同一份資料:

test inner i = 1
outer i before testFunc  101
func inner i = 102
outer i after testFunc 102
登入後複製

假設我們換個寫法,讓閉包外部環境中的變數在宣告閉包函數的之後,進行修改:

import "fmt"

func main() {
	sumFunc := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
	fmt.Println("先获取函数,不求结果")
	count := 0
	var sum = func() int {
		fmt.Println("第", count, "次求结果...")
		result := 0
		for _, v := range arr {
			result = result + v
		}
		return result
	}
	count = count + 100
	return sum
}
登入後複製

實際執行結果,count 會是修改後的值:

等待一会
第 100 次求结果...
结果: 15
登入後複製

這也證明了,實際上閉包並不會在宣告var sum = func() int {.. .}這句話之後,就將外部環境的count綁定到閉包中,而是在函數返回閉包函數的時候,才綁定的,這就是延遲綁定

如果还没看明白没关系,我们再来一个例子:

func main() {
	funcs := testFunc(100)
	for _, v := range funcs {
		v()
	}
}
func testFunc(x int) []func() {
	var funcs []func()
	values := []int{1, 2, 3}
	for _, val := range values {
		funcs = append(funcs, func() {
			fmt.Printf("testFunc val = %d\n", x+val)
		})
	}
	return funcs
}
登入後複製

上面的例子,我们闭包返回的是函数数组,本意我们想入每一个 val 都不一样,但是实际上 val都是一个值,==也就是执行到return funcs 的时候(或者真正执行闭包函数的时候)才绑定的 val值==(关于这一点,后面还有个Demo可以证明),此时 val的值是最后一个 3,最终输出结果都是 103:

testFunc val = 103
testFunc val = 103
testFunc val = 103
登入後複製

以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢?

下面的例子可以有效的解答:

import (
	"fmt"
	"time"
)

func main() {
	sumFunc := lazySum([]int{1, 2, 3, 4, 5})
	fmt.Println("等待一会")
	fmt.Println("结果:", sumFunc())
	time.Sleep(time.Duration(3) * time.Second)
	fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
	fmt.Println("先获取函数,不求结果")
	count := 0
	var sum = func() int {
		count++
		fmt.Println("第", count, "次求结果...")
		result := 0
		for _, v := range arr {
			result = result + v
		}
		return result
	}
	go func() {
		time.Sleep(time.Duration(1) * time.Second)
		count = count + 100
		fmt.Println("go func 修改后的变量 count:", count)
	}()
	return sum
}
登入後複製

输出结果如下:

先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
go func 修改后的变量 count: 101
第 102 次求结果...
结果: 15
登入後複製

第二次执行闭包函数的时候,明显 count被里面的 go func()修改了,也就是调用的时候,才真正的获取最新的外部环境,但是在声明的时候,就会把环境预留保存下来。

其实本质上,Go Routine的匿名函数的延迟绑定就是闭包的延迟绑定,上面的例子中,go func(){}获取到的就是最新的值,而不是原始值0

总结一下上面的验证点:

  • 闭包每次返回都是一个新的实例,每个实例都有一份自己的环境。
  • 同一个实例多次执行,会使用相同的环境。
  • 闭包如果逃逸的是指针,会相互影响,因为绑定的是指针,相同指针的内容修改会相互影响。
  • 闭包并不是在声明时绑定的值,声明后只是预留了外部环境(逃逸分析),真正执行闭包函数时,会获取最新的外部环境的值(也称为延迟绑定)。
  • Go Routine的匿名函数的延迟绑定本质上就是闭包的延迟绑定。

2、闭包的好处与坏处?

2.1 好处

纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:

Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?

A: 需要使用全局变量,让所有函数共享同一份变量。

但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):

  • 常驻于内存之中,只要程序不停会一直在内存中。
  • 污染全局,大家都可以访问,共享的同时不知道谁会改这个变量。

闭包可以一定程度优化这个问题:

  • 不需要使用全局变量,外部函数局部变量在闭包的时候会创建一份,生命周期与函数生命周期一致,闭包函数不再被引用的时候,就可以回收了。
  • 闭包暴露的局部变量,外界无法直接访问,只能通过函数操作,可以避免滥用。

除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。

2.2 坏处

函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。

闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。

3、闭包怎么实现的?

从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。

我们用以下的程序进行分析:

import "fmt"

func testFunc(i int) func() int {
	i = i * 2
	testFunc := func() int {
		i++
		return i
	}
	i = i * 2
	return testFunc
}
func main() {
	test := testFunc(1)
	fmt.Println(test())
}
登入後複製

执行结果如下:

5
登入後複製

先看看逃逸分析,用下面的命令行可以查看:

 go build --gcflags=-m main.go
登入後複製

一文淺析Golang中的閉包

可以看到 变量 i被移到堆中,也就是本来是局部变量,但是发生逃逸之后,从栈里面放到堆里面,同样的 test()函数由于是闭包函数,也逃逸到堆上。

下面我们用命令行来看看汇编代码:

go tool compile -N -l -S main.go
登入後複製

生成代码比较长,我截取一部分:

"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
        0x0000 00000 (main.go:5)        TEXT    "".testFunc(SB), ABIInternal, $56-8
        0x0000 00000 (main.go:5)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:5)        PCDATA  $0, $-2
        0x0004 00004 (main.go:5)        JLS     198
        0x000a 00010 (main.go:5)        PCDATA  $0, $-1
        0x000a 00010 (main.go:5)        SUBQ    $56, SP
        0x000e 00014 (main.go:5)        MOVQ    BP, 48(SP)
        0x0013 00019 (main.go:5)        LEAQ    48(SP), BP
        0x0018 00024 (main.go:5)        FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0018 00024 (main.go:5)        FUNCDATA        $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
        0x0018 00024 (main.go:5)        FUNCDATA        $5, "".testFunc.arginfo1(SB)
        0x0018 00024 (main.go:5)        MOVQ    AX, "".i+64(SP)
        0x001d 00029 (main.go:5)        MOVQ    $0, "".~r0+16(SP)
        0x0026 00038 (main.go:5)        LEAQ    type.int(SB), AX
        0x002d 00045 (main.go:5)        PCDATA  $1, $0
        0x002d 00045 (main.go:5)        CALL    runtime.newobject(SB)
        0x0032 00050 (main.go:5)        MOVQ    AX, "".&i+40(SP)
        0x0037 00055 (main.go:5)        MOVQ    "".i+64(SP), CX
        0x003c 00060 (main.go:5)        MOVQ    CX, (AX)
        0x003f 00063 (main.go:6)        MOVQ    "".&i+40(SP), CX
        0x0044 00068 (main.go:6)        MOVQ    "".&i+40(SP), DX
        0x0049 00073 (main.go:6)        MOVQ    (DX), DX
        0x004c 00076 (main.go:6)        SHLQ    $1, DX
        0x004f 00079 (main.go:6)        MOVQ    DX, (CX)
        0x0052 00082 (main.go:7)        LEAQ    type.noalg.struct { F uintptr; "".i *int }(SB), AX
        0x0059 00089 (main.go:7)        PCDATA  $1, $1
        0x0059 00089 (main.go:7)        CALL    runtime.newobject(SB)
        0x005e 00094 (main.go:7)        MOVQ    AX, ""..autotmp_3+32(SP)
        0x0063 00099 (main.go:7)        LEAQ    "".testFunc.func1(SB), CX
        0x006a 00106 (main.go:7)        MOVQ    CX, (AX)
        0x006d 00109 (main.go:7)        MOVQ    ""..autotmp_3+32(SP), CX
        0x0072 00114 (main.go:7)        TESTB   AL, (CX)
        0x0074 00116 (main.go:7)        MOVQ    "".&i+40(SP), DX
        0x0079 00121 (main.go:7)        LEAQ    8(CX), DI
        0x007d 00125 (main.go:7)        PCDATA  $0, $-2
        0x007d 00125 (main.go:7)        CMPL    runtime.writeBarrier(SB), $0
        0x0084 00132 (main.go:7)        JEQ     136
        0x0086 00134 (main.go:7)        JMP     142
        0x0088 00136 (main.go:7)        MOVQ    DX, 8(CX)
        0x008c 00140 (main.go:7)        JMP     149
        0x008e 00142 (main.go:7)        CALL    runtime.gcWriteBarrierDX(SB)
        0x0093 00147 (main.go:7)        JMP     149
        0x0095 00149 (main.go:7)        PCDATA  $0, $-1
        0x0095 00149 (main.go:7)        MOVQ    ""..autotmp_3+32(SP), CX
        0x009a 00154 (main.go:7)        MOVQ    CX, "".testFunc+24(SP)
        0x009f 00159 (main.go:11)       MOVQ    "".&i+40(SP), CX
        0x00a4 00164 (main.go:11)       MOVQ    "".&i+40(SP), DX
        0x00a9 00169 (main.go:11)       MOVQ    (DX), DX
        0x00ac 00172 (main.go:11)       SHLQ    $1, DX
        0x00af 00175 (main.go:11)       MOVQ    DX, (CX)
        0x00b2 00178 (main.go:12)       MOVQ    "".testFunc+24(SP), AX
        0x00b7 00183 (main.go:12)       MOVQ    AX, "".~r0+16(SP)
        0x00bc 00188 (main.go:12)       MOVQ    48(SP), BP
        0x00c1 00193 (main.go:12)       ADDQ    $56, SP
        0x00c5 00197 (main.go:12)       RET
        0x00c6 00198 (main.go:12)       NOP
        0x00c6 00198 (main.go:5)        PCDATA  $1, $-1
        0x00c6 00198 (main.go:5)        PCDATA  $0, $-2
        0x00c6 00198 (main.go:5)        MOVQ    AX, 8(SP)
        0x00cb 00203 (main.go:5)        CALL    runtime.morestack_noctxt(SB)
        0x00d0 00208 (main.go:5)        MOVQ    8(SP), AX
        0x00d5 00213 (main.go:5)        PCDATA  $0, $-1
        0x00d5 00213 (main.go:5)        JMP     0
登入後複製

可以看到闭包函数实际上底层也是用结构体new创建出来的:

一文淺析Golang中的閉包

使用的就是堆上面的 i

一文淺析Golang中的閉包

#也就是回傳函數的時候,實際上傳回結構體,結構體裡面記錄了函數的參考環境。

4、淺聊一下

4.1 Java 位元不支援閉包?

網路上有很多種看法,但實際上Java 雖然暫時不支援返回函數作為返參,但是Java 本質上還是實現了閉包的概念的,所使用的方式是內部類的形式,因為是內部類,所以相當於自帶了一個引用環境,算是一種不完整的閉包。

目前有一定限制,例如是final 宣告的,或是明確定義的值,才可以進行傳遞:

Stack Overflow上有相關答案:stackoverflow.com/questions/5…

一文淺析Golang中的閉包

#4.2 函數式程式設計的前景如何?

以下是Wiki的內容:

函數式程式設計長期以來在學術界流行,但幾乎沒有工業應用。造成這種局面的主要原因是函數式程式設計常被認為嚴重耗費CPU和記憶體資源[18] ,這是由於在早期實作函數式程式語言時並沒有考慮過效率問題,而且面向函數式程式設計特性,如保證參考透明性等,要求獨特的資料結構和演算法。 [19]

然而,最近幾種函數式程式語言已經在商業或工業系統中使用[20],例如:

  • Erlang,它由瑞典公司愛立信在20世紀80年代後期開發,最初用於實現容錯電信系統。此後,它已在NortelFacebookÉlectricité de FranceWhatsApp等公司作為流行語言創建一系列應用程式。 [21][22]
  • Scheme,它被用作早期Apple Macintosh電腦上的幾個應用程式的基礎,並且最近已應用於諸如訓練模擬軟體和望遠鏡控制等方向。
  • OCaml,它於1990年代中期推出,已經在金融分析,驅動程式驗證,工業機器人程式設計和嵌入式軟體靜態分析等領域得到了商業應用。
  • Haskell,它雖然最初是作為一種研究語言,也已被一系列公司應用於航空航天系統,硬體設計和網路程式設計等領域。

其他在工業中使用的函數式程式語言包括多範式的Scala[23]F#,還有Wolfram語言Common LispStandard MLClojure等。

從我個人的看法,不看好純函數編程,但是函數式編程的思想,我相信以後幾乎每門高級編程需要都會具備,特別期待 Java 擁抱函數式編程。從我自己了解的語言來看,像 Go,JavaScript 中的函數式程式設計的特性,都讓開發者深愛不已(當然,如果寫出了bug,就是深惡痛疾)。

最近突然火了一波的原因,也是因為世界不停的發展,記憶也越來越大,這個因素的限制幾乎要解放了。

我相信,世界是絢麗多彩的,要是一種事物統治世界,絕無可能,更多的是百家爭鳴,程式語言或者程式設計範式也一樣,後續可能有集大成者,最終最終歷史會篩選出最終符合人類社會發展的。

更多程式相關知識,請造訪:程式設計影片! !

以上是一文淺析Golang中的閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

C++ Lambda 表達式如何實作閉包? C++ Lambda 表達式如何實作閉包? Jun 01, 2024 pm 05:50 PM

C++Lambda表達式支援閉包,即保存函數作用域變數並供函數存取。語法為[capture-list](parameters)->return-type{function-body}。 capture-list定義要捕獲的變量,可以使用[=]按值捕獲所有局部變量,[&]按引用捕獲所有局部變量,或[variable1,variable2,...]捕獲特定變量。 Lambda表達式只能存取捕獲的變量,但無法修改原始值。

Go WebSocket 訊息如何發送? Go WebSocket 訊息如何發送? Jun 03, 2024 pm 04:53 PM

在Go中,可以使用gorilla/websocket包發送WebSocket訊息。具體步驟:建立WebSocket連線。傳送文字訊息:呼叫WriteMessage(websocket.TextMessage,[]byte("訊息"))。發送二進位訊息:呼叫WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

如何在 Go 中使用正規表示式匹配時間戳記? 如何在 Go 中使用正規表示式匹配時間戳記? Jun 02, 2024 am 09:00 AM

在Go中,可以使用正規表示式比對時間戳記:編譯正規表示式字串,例如用於匹配ISO8601時間戳記的表達式:^\d{4}-\d{2}-\d{2}T \d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$ 。使用regexp.MatchString函數檢查字串是否與正規表示式相符。

Golang 與 Go 語言的區別 Golang 與 Go 語言的區別 May 31, 2024 pm 08:10 PM

Go和Go語言是不同的實體,具有不同的特性。 Go(又稱Golang)以其並發性、編譯速度快、記憶體管理和跨平台優點而聞名。 Go語言的缺點包括生態系統不如其他語言豐富、文法更嚴格、缺乏動態類型。

Golang 技術效能優化中如何避免記憶體洩漏? Golang 技術效能優化中如何避免記憶體洩漏? Jun 04, 2024 pm 12:27 PM

記憶體洩漏會導致Go程式記憶體不斷增加,可通過:關閉不再使用的資源,如檔案、網路連線和資料庫連線。使用弱引用防止記憶體洩漏,當物件不再被強引用時將其作為垃圾回收目標。利用go協程,協程棧記憶體會在退出時自動釋放,避免記憶體洩漏。

閉包在 Java 中是如何實現的? 閉包在 Java 中是如何實現的? May 03, 2024 pm 12:48 PM

Java中的閉包允許內部函數存取外部的作用域變量,即使外部函數已經退出。透過匿名內部類別實現,內部類別持有一個外部類別的引用,使外部變數保持活動。閉包增強了程式碼靈活性,但需要注意記憶體洩漏風險,因為匿名內部類別對外部變數的參考會保持這些變數的活動狀態。

Go 並發函數的單元測試指南 Go 並發函數的單元測試指南 May 03, 2024 am 10:54 AM

對並發函數進行單元測試至關重要,因為這有助於確保其在並發環境中的正確行為。測試並發函數時必須考慮互斥、同步和隔離等基本原理。可以透過模擬、測試競爭條件和驗證結果等方法對並發函數進行單元測試。

如何使用 Golang 的錯誤包裝器? 如何使用 Golang 的錯誤包裝器? Jun 03, 2024 pm 04:08 PM

在Golang中,錯誤包裝器允許你在原始錯誤上追加上下文訊息,從而創建新錯誤。這可用於統一不同程式庫或元件拋出的錯誤類型,簡化偵錯和錯誤處理。步驟如下:使用errors.Wrap函數將原有錯誤包裝成新錯誤。新錯誤包含原始錯誤的上下文資訊。使用fmt.Printf輸出包裝後的錯誤,提供更多上下文和可操作性。在處理不同類型的錯誤時,使用errors.Wrap函數統一錯誤類型。

See all articles