[譯] Golang 語言中的函數類型

OmoruHadden 8年前發布 | 11K 次閱讀 Go語言 Google Go/Golang開發 UNIX

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

 

 本文由用戶 OmoruHadden 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!