[譯] Golang 語言中的函數類型
golang語言中的函數類型(翻譯)
多數的開發者對ruby,javascript,或者python這種暴露高階函數的動態語言是熟悉的。對于這些具有腳本背景的開發者,這是很難轉換成go的把那些概念,因為go的類型系統似乎就是一個阻礙。另外一面,這些問題也許是相反的,對于哪些具有靜態類型背景,原生的面向對象語言比如c++,或者java,靜態類型系統是少一些絆腳石的。但是高階函數的使用也似乎缺少直觀性。程序員對函數的經驗,大部分的認知留下的印象可能是非常缺乏想象力的。但是非常有益的是,這篇文章至少會闡述如何去使用go的類型系統中有關functions的概念。
這這篇文章中,我們將看幾個函數類型在go中非常有用的例子。讀者不需要假定是一個經驗老道的go開發者,只要大概的一些關于go的知識,在理解這篇文章上就會變的非常有用。</code></pre>
匿名函數和閉包
馬上,go支持匿名函數和閉包,對我來說,go對匿名函數的支持是很容易讓人聯想起javascript對匿名函數的支持,但是它不是python的lambads(僅僅是一條語句)對匿名函數脆弱的支持,或者是ruby的套件那樣有很多種方式去創建閉包(我不是ruby的粉絲,因此像這樣的 proc/block/lambda/Method 事情對我來說是非常復雜)。不管怎么說,在go中聲明匿名函數是非常的輕量:
func(){
fmt.Println("hello")
}
匿名函數只是作為一個表達式,那是非常沒有意義的,通常來說你想的是它被存到一個變量中,插入到數據結構里,或者把它傳遞到另外的函數當中。常常看到以下這種類型的代碼出現在go中:
fn:=func(){
fmt.Println("hello")
}
我們現在有一個變量fn,它是一個function;它的類型是func()。它能像其他的任何函數一樣被調用,通過說fn(),或者賦值給你感興趣的其他func()。當然,因為我們有對閉包的支持,我們也能引用一些定義在函數作用域當中的數據。
x:= 5
fn:=func(){
fmt.Println("x is",x)
}
fn()
x++
fn()
以上代碼片段的輸出如下:
x is 5
x is 6
run that example here 到目前為止,這些看上去非常像javascript中看到的一樣,而不是靜態類型。
函數集合
當然,我們能夠使用函數在所有我們能使用常規數據類型的地方。我們確實可以這樣,創建一個切片函數,隨機選取一個函數,然后執行它.我們定義了binFunc類型,它是一個二元一次函數,帶有兩個參數x,y,然后返回一個整數結果。對于這個例子來說這些都不是非常必要的。但是輸入binFunc比一而再到處可以看到的輸入func(int,int) int 是更加方便的。
type binFunc fun(int, int) int
func main() {
// seed your random number generator.
rand.Seed(time.Now().Unix())
// create a slice of functions
fns:= []binFunc{
func(x,yint) int { returnx+y},
func(x,yint) int { returnx-y},
func(x,yint) int { returnx*y},
func(x,yint) int { returnx/y},
func(x,yint) int { returnx%y},
}
// pick one of those functions at random
fn:=fns[rand.Intn(len(fns))]
// and execute it
x,y:= 12, 5
fmt.Println(fn(x,y))
}
</code></pre>
( run this example )
當然,這個例子是很作的,但是它闡述了go的核心概念:函數是一等公民(first class)
函數字段
同樣的,我們可能感興趣把function類型用在結構體的字段上。這樣做允許我們對一個函數加上一些額外的信息,比如運行時可以獲取的標簽。
type opstruct {
namestring
fn func(int, int) int
}
func main(){
// send your random number generator
rand.Seed(time.Now().Unix())
//create a slice of ops
ops:= []op{
{"add",func(x,yint) int {returnx+y}},
{"sub",func(x,yint) int {returnx-y}},
{"mul"func(x,yint) int {returnx*y}},
{"div"func(x,yint) int {returnx/y}},
{"mod"func(x,yint) int {returnx%y}},
}
//pick one of these ops at random
o:=ops[rand.Intn(len(ops))]
x,y:= 12, 5
fmt.Println(o.name,x,y)
fmt.Println(o.fn(x,y))
}
</code></pre>
function也能被存在map中,所以你也可以用map[string]binFunc做一些跟上面這個例子相似的事情。
遞歸函數類型
另外關于函數類型有趣的概念是它允許我們定義遞歸函數類型,他是一種操作自己本身的函數類型。就是說它既是函數的參數也是函數的返回值。使用這個技術你能做很多事情如果設計這樣的函數是困難的,因此為了說明清楚,我已經設計了一種執行“隨機步”的遞歸函數。是的,這是一個經過完整設計的例子,我們將看到一個walk函數,它具有一個整形指針參數,并且返回walk函數。
type walkFn func(*int)walkFn
這個函數的預期是去增加這個指針所指向的值。然后返回一個函數描述了下一步要執行的內容。以下的walkEqual是一個有效的walkfn函數的實例:
func walkEqual(i*int)walkFn{
*i+=rand.Intn(7) - 3
}
這個函數運行起來的效果會是i所指向的整數會隨機的+-3,然后函數的返回值是它自己。我們將會在下面執行這個隨機函數。
當然,這是非常有趣的。讓我們嵌套兩個以上的函數,walkForward和walkBackward,這個會保證我們往前走(正數)的方向,和往后走(負數)的方向。每一個函數都會執行它自己,然后隨機返回兩者中的一個。我們開啟一個小的但是完整的叫做pickRandom的函數。它將隨機的選擇一個作為輸入函數,并且返回它。
func pickRandom(fns...walkFn)walkFn{
returnfns[rand.Intn(len(fns))]
}
然后我們定義的三個walk函數,如下所示:
packagemain
import (
"fmt"
"math/rand"
"time"
)
type walkFn func(*int)walkFn
func pickRandom(fns...walkFn)walkFn{
returnfns[rand.Intn(len(fns))]
}
func walKEqual(iint)walkFn{
i+=rand.Intn(7) - 3
returnpickRandom(walkForward,walkBackward)
}
func walkForward(iint)walkFn{
i+=rand.Intn(6)
returnpickRandom(walKEqual,walkBackward)
}
func walkBackward(iint)walkFn{
i+= -rand.Intn(6)
returnpickRandom(walKEqual,walkForward)
}
func main() {
// time is frozen on playground, so this is actually always
// the same. The behavior is different when run locally.
rand.Seed(time.Now().Unix())
fn,progress:=walKEqual, 0
fori:= 0;i< 20;i++ {
fn=fn(&progress)
fmt.Println(progress)
}
}
random output:
2
1
6
6
7
2
6
2
7
6
8
11
13
13
11
16
15
17
22
17
</code></pre>
以上這個例子是不太合理的設計,因此我們要做的是關注遞歸函數的機制,在標準庫中,這個技術被用在text/template中生成lexer。
函數類型作為接口的值
在go中函數類型可以有方法,這是很難去看到的,首先,為什么這是有用的。這里有兩個方面的影響對于函數類型在go能有方法的事實。第一:因為任意類型能有防范就能滿足接口,在go中函數類型也可能封裝成有效的接口類型,第二:在go中方法既可以是指針或者值接收者,我們能使用函數指針的方法切換這個函數之鄉的方法調用的上下文。這樣說是有點讓人興奮的,所以讓我們看兩個事實。
首先,最明顯的做法是,使得一個函數類型滿足一個接口是選擇一個接口只有一個方法。實現它是簡單的。讓我們定義一個add function就想我們上面做過的,但是我們也給它一個Error() string方法,在go中任何類型具有Error() string方法就是有效的error類型,所以我們的函數既是一個function也是一個error。你可以運行這個沒什么用處的例子在你的終端上:
packagemain
import(
"fmt"
)
type binFunc func(int, int) int
func add(x,yint) int {returnx+y}
func(f binFunc) Error() string {return "binFunc error"}
func main() {
varerr error
err=binFunc(add)
fmt.Println(err)
}
</code></pre>
以上這個例子真的是沒什么用處,我們不得不去執行類型轉換,轉換類型是func(int, int) int的add函數成binFunc。這個看上去像是繁重的性能損耗在go運行時,但是如果有另外一個有效的方式把 func(int, int) int轉換成實現error:
type loudBinFunc func(int, int) int
func(f loudBinFunc) Error() string{
return "THIS ERROR IS A LOT LOUDER"
}
</code></pre>
如果binFunc和loudBinFunc都定義了,運行時就不會知道如何去轉換我們的add函數。及時接口只有一個有效的實現被給定的類型,運行時也不會自動的執行轉換。它看上去是煩惱的,但是也使得寫出沒有bug的代碼變的更容易。在標準庫的net/http包中,我們能看到使用函數類型的實現接口的例子,http.Handler類型被用在標準庫中注冊http.handlers的接口。
typeHandler interface{
ServerHTTP(ResponseWriter, *Request)
}
這里還有一種http.HandlerFunc類型,它只是包裝了func(http.ResponseWriter, *http.Request)以滿足http,Handler。這就使得我們可以用一個函數或者一個結構體去作為一個web的handler。最終的結果是我們同時獲得了http.Handler和http.HandleFunc函數,使得寫web的handler是更加的方便。
現在我們知道如何建立函數的方法從而允許我們去實現一個接口,我將展示一點難懂用法也是被證明非常有用的,讓我們截取一些在json文檔中的函數,在標準庫的encoding/json包是一般的方法用來編碼和解碼json文檔。這個包是簡歷在接口的基礎之上,它定義了一個json.Unmarshaler接口,用來去反序列化json文檔:
typeUnmarshaler interface{
UnmarshalJSON([]bye)error
}
如果你定義了這個方法, encoding/json包就會使用它去解析json數據,另外,它也試圖去弄清楚自己要做什么(一般都是正確的),讓我們拿出binFunc的例子,但是這次,我么要貼一些二元函數在json文檔中,我們開始是老的binFunc定義:
type binFunc func(int, int) int
因為函數本身沒有任何持久化的數據,我們需要一個地方去注冊名字。我們用map[string]binFunc去儲存和我們函數相關的名字:
varfnRegistry=map[string]binFunc{
"add":func(x,yint) int { returnx+y},
"sub":func(x,yint) int { returnx-y},
"mul":func(x,yint) int { returnx*y},
"div":func(x,yint) int { returnx/y},
"mod":func(x,yint) int { returnx%y},
}
在go中,如果我們試圖用不存在的key從map中獲得value,這種操作將返回一個“0”值。對于一個函數類型,零值就意味著nil,我們只要做一個測試,賦一個nil去看我們將得到什么,現在我們要在binFunc類型上實現UnmarshalJSON方法。因為我們想要現在調用這個函數的看到一些不一樣的。用指針類型實現UnmarshalJSON,并且因為我們知道我們想要一個字符串,我們只要托載一些存在的反序列化去兼容標準的json包。
有了這些定義,我們就能從json文檔中反序列化到binFunc。接下來就是一個相對完整的例子。
packagemain
import(
"encoding/json"
"fmt"
"log"
"math/rand"
"time"
)
varjsonDoc= []byte(["add", "sub", "mul", "div"]
)
varregistry=map[string]binFunc{
"add":func(x,yint) int { returnx+y},
"sub":func(x,yint) int { returnx-y},
"mul":func(x,yint) int { returnx*y},
"div":func(x,yint) int { returnx/y},
}
type binFunc func(int, int) int
//實現了下面這個方法就可以自己反序列化自己
func(fn*binFunc) UnmarshalJSON(b[]byte)error{
varnamestring
iferr:=json.Umarshal(b, &name);err!= nil {
returnerr
}
// get the function out of our function registry
found:=registry[name]
iffound== nil {
// return a descriptive error if we can't find the function
returnfmt.Errorf("unknow function in (binFunc)UnmarshalJSON: %s",name)
}
// dereference the pointer receiver, so that the changes are visible to the caller
fn=found
return nil
}
func main() {
rand.Seed(time.Now().Unix())
varfns[]binFunc
iferr:=json.Unmarshal(jsonDoc, &fns);err!= nil {
log.Fatal(err)
}
fn:=fns[rand.Intn(len(fns))]
x:=fn(12, 5)
fmt.Println(x)
}
</code></pre>
函數和channel
最后這個模式在go中是非常的特別的,而且這是你用channel組合函數發生的結果。完整的解釋go的并發模型超出本文的范圍。但是如果不想要雨里霧里對此,我推薦閱讀這篇文章 the concurrency section in effective go ,如果還有時間,看看這個非常棒的google io, go concurrency patterns ,從先前的觀點,一些關于go的知識還是要假設存在的。
因為channel是go的原語,并且我們能用其他的類型組成channel,我們甚至能用function組成channel,因為函數可以匿名,并且函數也可以閉包,用這個三個屬性我們就可以組合成匿名閉包channel,對于這個channel類型的定義,可以讓我們很容易意識到:chan func().保持住這篇文章的主題,讓我們創建一個切片函數,但我們會讓每一個函數都閉包:
x:= 10
fns:= []func(){
func() {x+= 1 },
func() {x-= 1 },
func() {x*= 2 },
func() {x/= 2 },
func() {x*=x},
}
我們也捕捉我們之前看到過的代碼片段,并且給出一種隨機獲取函數的機制:
func pickFunc(fns...func())func() {
returnfns[rand.Intn(len(fns))]
}
接下來,我們定義一個func()類型的channel:
c:=make(chan func())
并且我們定義了一個拿它自己當參數的函數channel的函數,數量描述了需要寫入這個channel的函數的個數,然后就是一組候選函數。
在這個函數開始的時候,我們用defer close(c)確保我們在生產完數據之后關閉channel。在這里我們只使用常規的for循環,使用pickFunc隨機獲取一個函數,然后將我們給到的function寫入channel,在接收端,我們使用range去讀取channel中的值,取到值之后就立刻執行這個函數,通過在每一次讀取值之后添加休眠函數,我們可以為任意閉包函數實現速率控制。
forfn:=range c{
fn()
fmt.Println(x)
time.Sleep(delay)
}
將這些放在一起,我們得到了下面的程序:
packagemain
import(
"fmt"
"math/rand"
"time"
)
vardelay= 200*time.Millisecond
func pickFunc(fns...func())func(){
returnfns[rand.Intn(len(fns))]
}
func produce(c chan func(),nint,fns...func()){
defer close(c)
fori:= 0;i<n;i++ {
c<-pickFunc(fns...)
}
}
func main() {
// time is varibal
rand.Seed(time.Now().Unix())
x:= 10
fns:= []func(){
func() {x+=1},
func() {x+=1},
func() {x=2},
func() {x/=2},
func() {x=x},
}
c:=make(chan func())
go produce(c, 10,fns...)
forfn:=range c{
fn()
fmt.Println(x)
time.Sleep(delay)
}
}
random output:
20
10
11
22
44
1936
1937
1938
3755844
3755845
</code></pre>
或者你也可以
packagemain
import (
"fmt"
"math/rand"
"time"
)
vardelay= 200 *time.Millisecond
func pickFunc(fns...binFunc)binFunc{
returnfns[rand.Intn(len(fns))]
}
func produce(c chan binFunc,nint,fns...binFunc) {
defer close(c)
fori:= 0;i<n;i++ {
c<-pickFunc(fns...)
}
}
type binFunc func(int, int) int
func main() {
// time is varibal
rand.Seed(time.Now().Unix())
x:= 10
y:= 2
fns:= []binFunc{
func(x,yint) int { returnx+y},
func(x,yint) int { returnx+y},
func(x,yint) int { returnx+y},
func(x,yint) int { returnx+y},
func(x,yint) int { returnx+y},
}
c:=make(chan binFunc)
go produce(c, 10,fns...)
forfn:=range c{
fn(x,y)
fmt.Println(x)
time.Sleep(delay)
}
}
</code></pre>
完結
原文鏈接: golang function type
來自:https://www.zybuluo.com/aliasliyu4/note/567290