Go的研習筆記-day5(以Java的視角學習Go)

原文鏈接:https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.12.md

函數:也就是Go裏面的基本代碼塊,與Java中的方法類似

Go是編譯型語言,所以函數編寫的順序是無關緊要的;鑑於可讀性的需求,最好把 main() 函數寫在文件的前面,其他函數按照一定邏輯順序進行編寫(例如函數被調用的順序)。
編寫多個函數的主要目的是將一個需要很多行代碼的複雜問題分解爲一系列簡單的任務(那就是函數)來解決。而且,同一個任務(函數)可以被調用多次,有助於代碼重用。
簡單的 return 語句也可以用來結束 for 死循環,或者結束一個協程(goroutine)。
Go 裏面有三種類型的函數:

  • 普通的帶有名字的函數
  • 匿名函數或者lambda函數
  • 方法

除了main()、init()函數外,其它所有類型的函數都可以有參數與返回值。函數參數、返回值以及它們的類型被統稱爲函數簽名。
函數被調用的基本格式如下:
pack1.Function(arg1, arg2, …, argn)
Function 是 pack1 包裏面的一個函數,括號裏的是被調用函數的實參(argument):這些值被傳遞給被調用函數的形參(parameter)。函數被調用的時候,這些實參將被複制(簡單而言)然後傳遞給被調用函數。函數一般是在其他函數裏面被調用的,這個其他函數被稱爲調用函數(calling function)。函數能多次調用其他函數,這些被調用函數按順序(簡單而言)執行,理論上,函數調用其他函數的次數是無窮的(直到函數調用棧被耗盡)。這裏表明也會出現Java中的棧溢出錯誤。
函數重載(function overloading)指的是可以編寫多個同名函數,只要它們擁有不同的形參與/或者不同的返回值,在 Go 裏面函數重載是不被允許的。這將導致一個編譯錯誤:
funcName redeclared in this book, previous declaration at lineno
這裏與Java中重載方法不同需要注意
Go 語言不支持這項特性的主要原因是函數重載需要進行多餘的類型匹配影響性能;沒有重載意味着只是一個簡單的函數調度。所以你需要給不同的函數使用不同的名字。如果需要申明一個在外部定義的函數,你只需要給出函數名與函數簽名,不需要給出函數體:
func flushICache(begin, end uintptr) // implemented externally
函數也可以以申明的方式被使用,作爲一個函數類型,就像:
type binOp func(int, int) int
在這裏,不需要函數體 {}。
函數是一等值(first-class value):它們可以賦值給變量,就像 add := binOp 一樣。
這個變量知道自己指向的函數的簽名,所以給它賦一個具有不同簽名的函數值是不可能的。
函數值(functions value)之間可以相互比較:如果它們引用的是相同的函數或者都是 nil 的話,則認爲它們是相同的函數。函數不能在其它函數裏面聲明(不能嵌套),不過我們可以通過使用匿名函數來破除這個限制。
本來go沒有泛型概念,但是現在也將支持。

  • 函數參數與返回值
    函數能夠接收參數供自己使用,也可以返回零個或多個值(我們通常把返回多個值稱爲返回一組值)。相比與 C、C++、Java 和 C#,多值返回是 Go 的一大特性,爲我們判斷一個函數是否正常執行提供了方便。
    我們通過 return 關鍵字返回一組值。事實上,任何一個有返回值(單個或多個)的函數都必須以 return 或 panic結尾。
    在函數塊裏面,return 之後的語句都不會執行。如果一個函數需要返回值,那麼這個函數裏面的每一個代碼分支(code-path)都要有 return 語句。
    函數定義時,它的形參一般是有名字的,不過我們也可以定義沒有形參名的函數,只有相應的形參類型,就像這樣:func f(int, int, float64)。
    沒有參數的函數通常被稱爲 niladic 函數(niladic function),就像 main.main()。
  • 按值傳遞(call by value) 按引用傳遞(call by reference)
  • Go 默認使用按值傳遞來傳遞參數,也就是傳遞參數的副本。函數接收參數副本之後,在使用變量的過程中可能對副本的值進行更改,但不會影響到原來的變量,比如 Function(arg1)。
    如果你希望函數可以直接修改參數的值,而不是對參數的副本進行操作,你需要將參數的地址(變量名前面添加&符號,比如 &variable)傳遞給函數,這就是按引用傳遞,比如 Function(&arg1),此時傳遞給函數的是一個指針。如果傳遞給函數的是一個指針,指針的值(一個地址)會被複制,但指針的值所指向的地址上的值不會被複制;我們可以通過這個指針的值來修改這個值所指向的地址上的值。(譯者注:指針也是變量類型,有自己的地址和值,通常指針的值指向一個變量的地址。所以,按引用傳遞也是按值傳遞。)
    幾乎在任何情況下,傳遞指針(一個32位或者64位的值)的消耗都比傳遞副本來得少。
    在函數調用時,像切片(slice)、字典(map)、接口(interface)、通道(channel)這樣的引用類型都是默認使用引用傳遞(即使沒有顯式的指出指針)。
    有些函數只是完成一個任務,並沒有返回值。我們僅僅是利用了這種函數的副作用,就像輸出文本到終端,發送一個郵件或者是記錄一個錯誤等。
    但是絕大部分的函數還是帶有返回值的。
  • 命名的返回值(named return variables)
    儘量使用命名返回值:會使代碼更清晰、更簡短,同時更加容易讀懂。
package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
    numx2, numx3 = getX2AndX3(num)
    PrintValues()
    numx2, numx3 = getX2AndX3_2(num)
    PrintValues()
}

func PrintValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
}
  • 空白符
    空白符用來匹配一些不需要的值,然後丟棄掉
package main

import "fmt"

func main() {
    var i1 int
    var f1 float32
    i1, _, f1 = ThreeValues()
    fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}

func ThreeValues() (int, int, float32) {
    return 5, 6, 7.5
}
輸出結果:

The int: 5, the float: 7.500000
  • 改變外部變量
    傳遞指針給函數不但可以節省內存(因爲沒有複製變量的值),而且賦予了函數直接修改外部變量的能力,所以被修改的變量不再需要使用 return 返回。如下的例子,reply 是一個指向 int 變量的指針,通過這個指針,我們在函數內修改了這個 int 變量的數值。
package main

import (
    "fmt"
)

// this function changes reply:
func Multiply(a, b int, reply *int) {
    *reply = a * b
}

func main() {
    n := 0
    reply := &n
    Multiply(10, 5, reply)
    fmt.Println("Multiply:", *reply) // Multiply: 50
}
  • 傳遞變長參數
    在Java中變長參數和go語言中一樣都是通過…來定義的
    func myFunc(a, b, arg …int) {}
    這個函數接受一個類似某個類型的 slice 的參數
    func Greeting(prefix string, who …string)
    Greeting(“hello:”, “Joe”, “Anna”, “Eileen”)
    在 Greeting 函數中,變量 who 的值爲 []string{“Joe”, “Anna”, “Eileen”}。
    如果參數被存儲在一個 slice 類型的變量 slice 中,則可以通過 slice… 的形式來傳遞參數,調用變參函數。
    如果變長參數的類型並不是都相同的,假設現在使用 5 個參數來進行傳遞,有 2 種方案可以解決這個問題:
  • 使用結構
    定義一個結構類型,假設它叫 Options,用以存儲所有可能的參數:
type Options struct {
	par1 type1,
	par2 type2,
	...
}
  • 使用空接口:
    如果一個變長參數的類型沒有被指定,則可以使用默認的空接口 interface{},這樣就可以接受任何類型的參數。該方案不僅可以用於長度未知的參數,還可以用於任何不確定類型的參數。一般而言我們會使用一個 for-range 循環以及 switch 結構對每個參數的類型進行判斷:
func typecheck(..,..,values … interface{}) {
	for _, value := range values {
		switch v := value.(type) {
			case int: …
			case float: …
			case string: …
			case bool: …
			default: …
		}
	}
}
  • defer 和追蹤
    關鍵字 defer 的用法類似於面向對象編程語言 Java 和 C# 的 finally 語句塊,它一般用於釋放某些已分配的資源。
    當有多個 defer 行爲被註冊時,它們會以逆序執行(類似棧,即後進先出):
func f() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%d ", i)
	}
}
上面的代碼將會輸出:4 3 2 1 0。
關鍵字 defer 允許我們進行一些函數執行完成後的收尾工作,例如:
關閉文件流 
// open a file  
defer file.Close()
解鎖一個加鎖的資源 
mu.Lock()  
defer mu.Unlock() 
打印最終報告
printHeader()  
defer printFooter()
關閉數據庫鏈接
// open a database connection  
defer disconnectFromDB()
合理使用 defer 語句能夠使得代碼更加簡潔。
  • 使用 defer 語句實現代碼追蹤
    一個基礎但十分實用的實現代碼執行追蹤的方案就是在進入和離開某個函數打印相關的消息,即可以提煉爲下面兩個函數:
    func trace(s string) { fmt.Println(“entering:”, s) }
    func untrace(s string) { fmt.Println(“leaving:”, s) }
  • 使用 defer 語句來記錄函數的參數與返回值
package main

import (
	"io"
	"log"
)

func func1(s string) (n int, err error) {
	defer func() {
		log.Printf("func1(%q) = %d, %v", s, n, err)
	}()
	return 7, io.EOF
}

func main() {
	func1("Go")
}

內置函數
Go 語言擁有一些不需要進行導入操作就可以使用的內置函數。它們有時可以針對不同的類型進行操作,例如:len、cap 和 append,或必須用於系統級的操作,例如:panic。因此,它們需要直接獲得編譯器的支持。
在這裏插入圖片描述
遞歸函數
棧溢出:一般出現在大量的遞歸調用導致的程序棧內存分配耗盡。這個問題可以通過一個名爲懶惰求值的技術解決,在 Go 語言中,我們可以使用管道(channel)和 goroutine來實現。通過這個方案也可以優化斐波那契數列的生成問題。

將函數作爲參數
函數可以作爲其它函數的參數進行傳遞,然後在其它函數內調用執行,一般稱之爲回調。下面是一個將函數作爲參數的簡單例子(function_parameter.go):
將函數作爲參數的最好的例子是函數 strings.IndexFunc():
該函數的簽名是 func IndexFunc(s string, f func(c rune) bool) int,它的返回值是在函數 f© 返回 true、-1 或從未返回時的索引值。
例如 strings.IndexFunc(line, unicode.IsSpace) 就會返回 line 中第一個空白字符的索引值

閉包
當我們不希望給函數起名字的時候,可以使用匿名函數,例如:func(x, y int) int { return x + y }。
這樣的一個函數不能夠獨立存在(編譯器會返回錯誤:non-declaration statement outside function body),但可以被賦值於某個變量,即保存函數的地址到變量中:fplus := func(x, y int) int { return x + y },然後通過變量名對函數進行調用:fplus(3,4)。
當然,您也可以直接對匿名函數進行調用:func(x, y int) int { return x + y } (3, 4)。
下面是一個計算從 1 到 1 百萬整數的總和的匿名函數:

func() {
	sum := 0
	for i := 1; i <= 1e6; i++ {
		sum += i
	}
}()

defer 語句和匿名函數
關鍵字 defer 經常配合匿名函數使用,它可以用於改變函數的命名返回值。
匿名函數還可以配合 go 關鍵字來作爲 goroutine 使用
匿名函數同樣被稱之爲閉包(函數式語言的術語):它們被允許調用定義在其它環境下的變量。閉包可使得某個函數捕捉到一些外部狀態,例如:函數被創建時的狀態。另一種表示方式爲:一個閉包繼承了函數所聲明時的作用域。這種狀態(作用域內的變量)都被共享到閉包的環境中,因此這些變量可以在閉包中被操作,直到被銷燬。閉包經常被用作包裝函數:它們會預先定義好 1 個或多個參數以用於包裝,詳見下一節中的示例。另一個不錯的應用就是使用閉包來完成更加簡潔的錯誤檢查

應用閉包:將函數作爲返回值
在閉包中使用到的變量可以是在閉包函數體內聲明的,也可以是在外部函數聲明的:

 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
可以返回其它函數的函數和接受其它函數作爲參數的函數均被稱之爲高階函數,是函數式語言的特點。函數也是一種值,因此很顯然 Go 語言具有一些函數式語言的特性。閉包在 Go 語言中非常常見,常用於 goroutine 和管道操作。Go 語言中的函數在處理混合對象時也有強大能力。

使用閉包調試
當您在分析和調試複雜的程序時,無數個函數在不同的代碼文件中相互調用,如果這時候能夠準確地知道哪個文件中的具體哪個函數正在執行,對於調試是十分有幫助的。您可以使用 runtime 或 log 包中的特殊函數來實現這樣的功能。包 runtime 中的函數 Caller() 提供了相應的信息,因此可以在需要的時候實現一個 where() 閉包函數來打印函數執行的位置:

where := func() {
	_, file, line, _ := runtime.Caller(1)
	log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

計算函數執行時間
time 包中的 Now() 和 Sub 函數:
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf(“longCalculation took this amount of time: %s\n”, delta)

通過內存緩存來提升性能
當在進行大量的計算時,提升性能最直接有效的一種方式就是避免重複計算。通過在內存中緩存和重複利用相同計算的結果,稱之爲內存緩存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章