Go 系列教程-2 基礎知識
Go 系列教程 —— 6. 函式(Function)
函式是什麼?
函式是一塊執行特定任務的程式碼。一個函式是在輸入源基礎上,通過執行一系列的演算法,生成預期的輸出。
函式的宣告
在 Go 語言中,函式宣告通用語法如下:
func functionname(parametername type) returntype {
// 函式體(具體實現的功能)
}
函式的宣告以關鍵詞 func
開始,後面緊跟自定義的函式名 functionname (函式名)
。函式的引數列表定義在 (
和 )
之間,返回值的型別則定義在之後的 returntype (返回值型別)
(parameter1 type, parameter2 type) 即(引數1 引數1的型別,引數2 引數2的型別)
的形式指定。之後包含在 {
和 }
之間的程式碼,就是函式體。
函式中的引數列表和返回值並非是必須的,所以下面這個函式的宣告也是有效的
func functionname() {
// 譯註: 表示這個函式不需要輸入引數,且沒有返回值
}
示例函式
我們以寫一個計算商品價格的函式為例,輸入引數是單件商品的價格和商品的個數,兩者的乘積為商品總價,作為函式的輸出值。
func calculateBill(price int, no int) int {
var totalPrice = price * no // 商品總價 = 商品單價 * 數量
return totalPrice // 返回總價
}
上述函式有兩個整型的輸入 price
和 no
,返回值 totalPrice
為 price
和 no
的乘積,也是整數型別。
如果有連續若干個引數,它們的型別一致,那麼我們無須一一羅列,只需在最後一個引數後新增該型別。 例如,price int, no int
price, no int
,所以示例函式也可寫成
func calculateBill(price, no int) int {
var totalPrice = price * no
return totalPrice
}
現在我們已經定義了一個函式,我們要在程式碼中嘗試著呼叫它。呼叫函式的語法為 functionname(parameters)
。呼叫示例函式的方法如下:
calculateBill(10, 5)
完成了示例函式宣告和呼叫後,我們就能寫出一個完整的程式,並把商品總價列印在控制檯上:
package main
import (
"fmt"
)
func calculateBill(price, no int) int {
var totalPrice = price * no
return totalPrice
}
func main() {
price, no := 90, 6 // 定義 price 和 no,預設型別為 int
totalPrice := calculateBill(price, no)
fmt.Println("Total price is", totalPrice) // 列印到控制檯上
}
該程式在控制檯上列印的結果為
Total price is 540
多返回值
Go 語言支援一個函式可以有多個返回值。我們來寫個以矩形的長和寬為輸入引數,計算並返回矩形面積和周長的函式 rectProps
。矩形的面積是長度和寬度的乘積, 周長是長度和寬度之和的兩倍。即:
面積 = 長 * 寬
周長 = 2 * ( 長 + 寬 )
package main
import (
"fmt"
)
func rectProps(length, width float64)(float64, float64) {
var area = length * width
var perimeter = (length + width) * 2
return area, perimeter
}
func main() {
area, perimeter := rectProps(10.8, 5.6)
fmt.Printf("Area %f Perimeter %f", area, perimeter)
}
如果一個函式有多個返回值,那麼這些返回值必須用 (
和 )
括起來。func rectProps(length, width float64)(float64, float64)
示例函式有兩個 float64 型別的輸入引數 length
和 width
,並返回兩個 float64 型別的值。該程式在控制檯上列印結果為
Area 60.480000 Perimeter 32.800000
命名返回值
從函式中可以返回一個命名值。一旦命名了返回值,可以認為這些值在函式第一行就被宣告為變量了。
上面的 rectProps 函式也可用這個方式寫成:
func rectProps(length, width float64)(area, perimeter float64) {
area = length * width
perimeter = (length + width) * 2
return // 不需要明確指定返回值,預設返回 area, perimeter 的值
}
請注意, 函式中的 return 語句沒有顯式返回任何值。由於 area 和 perimeter 在函式宣告中指定為返回值, 因此當遇到 return 語句時, 它們將自動從函式返回。
空白符
_ 在 Go 中被用作空白符,可以用作表示任何型別的任何值。
我們繼續以 rectProps
函式為例,該函式計算的是面積和周長。假使我們只需要計算面積,而並不關心周長的計算結果,該怎麼呼叫這個函式呢?這時,空白符 _ 就上場了。
下面的程式我們只用到了函式 rectProps
的一個返回值 area
package main
import (
"fmt"
)
func rectProps(length, width float64) (float64, float64) {
var area = length * width
var perimeter = (length + width) * 2
return area, perimeter
}
func main() {
area, _ := rectProps(10.8, 5.6) // 返回值周長被丟棄
fmt.Printf("Area %f ", area)
}
在程式的
area, _ := rectProps(10.8, 5.6)
這一行,我們看到空白符_
用來跳過不要的計算結果。
Go 系列教程 —— 7. 包
Noluye · 2017-12-08 14:11:32 · 7841 次點選 · 預計閱讀時間 9 分鐘 · 不到1分鐘之前 開始瀏覽
這是一個創建於 2017-12-08 14:11:32 的文章,其中的資訊可能已經有所發展或是發生改變。
這是 Golang 系列教程的第 7 個教程。
什麼是包,為什麼使用包?
到目前為止,我們看到的 Go 程式都只有一個檔案,檔案裡包含一個 main 函式和幾個其他的函式。在實際中,這種把所有原始碼編寫在一個檔案的方法並不好用。以這種方式編寫,程式碼的重用和維護都會很困難。而包(Package)解決了這樣的問題。
包用於組織 Go 原始碼,提供了更好的可重用性與可讀性。由於包提供了程式碼的封裝,因此使得 Go 應用程式易於維護。
例如,假如我們正在開發一個 Go 影象處理程式,它提供了影象的裁剪、銳化、模糊和彩色增強等功能。一種組織程式的方式就是根據不同的特性,把程式碼放到不同的包中。比如裁剪可以是一個單獨的包,而銳化是另一個包。這種方式的優點是,由於彩色增強可能需要一些銳化的功能,因此彩色增強的程式碼只需要簡單地匯入(我們會在隨後討論)銳化功能的包,就可以使用銳化的功能了。這樣的方式使得程式碼易於重用。
我們會逐步構建一個計算矩形的面積和對角線的應用程式。
通過這個程式,我們會更好地理解包。
main 函式和 main 包
所有可執行的 Go 程式都必須包含一個 main 函式。這個函式是程式執行的入口。main 函式應該放置於 main 包中。
package packagename
這行程式碼指定了某一原始檔屬於一個包。它應該放在每一個原始檔的第一行。
下面開始為我們的程式建立一個 main 函式和 main 包。在 Go 工作區內的 src 資料夾中建立一個資料夾,命名為 geometry
。在 geometry
資料夾中建立一個 geometry.go
檔案。
在 geometry.go 中編寫下面程式碼。
// geometry.go
package main
import "fmt"
func main() {
fmt.Println("Geometrical shape properties")
}
package main
這一行指定該檔案屬於 main 包。import "packagename"
語句用於匯入一個已存在的包。在這裡我們匯入了 fmt
包,包內含有 Println 方法。接下來是 main 函式,它會列印 Geometrical shape properties
。
鍵入 go install geometry
,編譯上述程式。該命令會在 geometry
資料夾內搜尋擁有 main 函式的檔案。在這裡,它找到了 geometry.go
。接下來,它編譯併產生一個名為 geometry
(在 windows 下是 geometry.exe
)的二進位制檔案,該二進位制檔案放置於工作區的 bin 資料夾。現在,工作區的目錄結構會是這樣:
src
geometry
gemometry.go
bin
geometry
鍵入 workspacepath/bin/geometry
,執行該程式。請用你自己的 Go 工作區來替換 workspacepath
。這個命令會執行 bin 資料夾裡的 geometry
二進位制檔案。你應該會輸出 Geometrical shape properties
。
建立自定義的包
我們將組織程式碼,使得所有與矩形有關的功能都放入 rectangle
包中。
我們會建立一個自定義包 rectangle
,它有一個計算矩形的面積和對角線的函式。
屬於某一個包的原始檔都應該放置於一個單獨命名的資料夾裡。按照 Go 的慣例,應該用包名命名該資料夾。
因此,我們在 geometry
資料夾中,建立一個命名為 rectangle
的資料夾。在 rectangle
資料夾中,所有檔案都會以 package rectangle
作為開頭,因為它們都屬於 rectangle 包。
在我們之前建立的 rectangle 資料夾中,再建立一個名為 rectprops.go
的檔案,新增下列程式碼。
// rectprops.go
package rectangle
import "math"
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
在上面的程式碼中,我們建立了兩個函式用於計算 Area
和 Diagonal
。矩形的面積是長和寬的乘積。矩形的對角線是長與寬平方和的平方根。math
包下面的 Sqrt
函式用於計算平方根。
注意到函式 Area 和 Diagonal 都是以大寫字母開頭的。這是有必要的,我們將會很快解釋為什麼需要這樣做。
匯入自定義包
為了使用自定義包,我們必須要先匯入它。匯入自定義包的語法為 import path
。我們必須指定自定義包相對於工作區內 src
資料夾的相對路徑。我們目前的資料夾結構是:
src
geometry
geometry.go
rectangle
rectprops.go
import "geometry/rectangle"
這一行會匯入 rectangle 包。
在 geometry.go
裡面新增下面的程式碼:
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 匯入自定義包
)
func main() {
var rectLen, rectWidth float64 = 6, 7
fmt.Println("Geometrical shape properties")
/*Area function of rectangle package used*/
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
/*Diagonal function of rectangle package used*/
fmt.Printf("diagonal of the rectangle %.2f ", rectangle.Diagonal(rectLen, rectWidth))
}
上面的程式碼匯入了 rectangle
包,並呼叫了裡面的 Area 和 Diagonal 函式,得到矩形的面積和對角線。Printf 內的格式說明符 %.2f
會將浮點數截斷到小數點兩位。應用程式的輸出為:
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
匯出名字(Exported Names)
我們將 rectangle 包中的函式 Area 和 Diagonal 首字母大寫。在 Go 中這具有特殊意義。在 Go 中,任何以大寫字母開頭的變數或者函式都是被匯出的名字。其它包只能訪問被匯出的函式和變數。在這裡,我們需要在 main 包中訪問 Area 和 Diagonal 函式,因此會將它們的首字母大寫。
在 rectprops.go
中,如果函式名從 Area(len, wid float64)
變為 area(len, wid float64)
,並且在 geometry.go
中, rectangle.Area(rectLen, rectWidth)
變為 rectangle.area(rectLen, rectWidth)
, 則該程式執行時,編譯器會丟擲錯誤 geometry.go:11: cannot refer to unexported name rectangle.area
。因為如果想在包外訪問一個函式,它應該首字母大寫。
init 函式
所有包都可以包含一個 init
函式。init 函式不應該有任何返回值型別和引數,在我們的程式碼中也不能顯式地呼叫它。init 函式的形式如下:
func init() {
}
init 函式可用於執行初始化任務,也可用於在開始執行之前驗證程式的正確性。
包的初始化順序如下:
- 首先初始化包級別(Package Level)的變數
- 緊接著呼叫 init 函式。包可以有多個 init 函式(在一個檔案或分佈於多個檔案中),它們按照編譯器解析它們的順序進行呼叫。
如果一個包匯入了另一個包,會先初始化被匯入的包。
儘管一個包可能會被匯入多次,但是它只會被初始化一次。
為了理解 init 函式,我們接下來對程式做了一些修改。
首先在 rectprops.go
檔案中添加了一個 init 函式。
// rectprops.go
package rectangle
import "math"
import "fmt"
/*
* init function added
*/
func init() {
fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
我們添加了一個簡單的 init 函式,它僅列印 rectangle package initialized
。
現在我們來修改 main 包。我們知道矩形的長和寬都應該大於 0,我們將在 geometry.go
中使用 init 函式和包級別的變數來檢查矩形的長和寬。
修改 geometry.go
檔案如下所示:
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 匯入自定義包
"log"
)
/*
* 1. 包級別變數
*/
var rectLen, rectWidth float64 = 6, 7
/*
*2. init 函式會檢查長和寬是否大於0
*/
func init() {
println("main package initialized")
if rectLen < 0 {
log.Fatal("length is less than zero")
}
if rectWidth < 0 {
log.Fatal("width is less than zero")
}
}
func main() {
fmt.Println("Geometrical shape properties")
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
我們對 geometry.go
做了如下修改:
- 變數 rectLen 和 rectWidth 從 main 函式級別移到了包級別。
- 添加了 init 函式。當 rectLen 或 rectWidth 小於 0 時,init 函式使用 log.Fatal 函式列印一條日誌,並終止了程式。
main 包的初始化順序為:
- 首先初始化被匯入的包。因此,首先初始化了 rectangle 包。
- 接著初始化了包級別的變數 rectLen 和 rectWidth。
- 呼叫 init 函式。
- 最後呼叫 main 函式。
當執行該程式時,會有如下輸出。
rectangle package initialized
main package initialized
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
果然,程式會首先呼叫 rectangle 包的 init 函式,然後,會初始化包級別的變數 rectLen 和 rectWidth。接著呼叫 main 包裡的 init 函式,該函式檢查 rectLen 和 rectWidth 是否小於 0,如果條件為真,則終止程式。我們會在單獨的教程裡深入學習 if 語句。現在你可以認為 if rectLen < 0
能夠檢查 rectLen
是否小於 0,並且如果是,則終止程式。rectWidth
條件的編寫也是類似的。在這裡兩個條件都為假,因此程式繼續執行。最後呼叫了 main 函式。
讓我們接著稍微修改這個程式來學習使用 init 函式。
將 geometry.go
中的 var rectLen, rectWidth float64 = 6, 7
改為 var rectLen, rectWidth float64 = -6, 7
。我們把 rectLen
初始化為負數。
現在當執行程式時,會得到:
rectangle package initialized
main package initialized
2017/04/04 00:28:20 length is less than zero
像往常一樣, 會首先初始化 rectangle 包,然後是 main 包中的包級別的變數 rectLen 和 rectWidth。rectLen 為負數,因此當執行 init 函式時,程式在列印 length is less than zero
後終止。
本程式碼可以在 github 下載。
使用空白識別符號(Blank Identifier)
匯入了包,卻不在程式碼中使用它,這在 Go 中是非法的。當這麼做時,編譯器是會報錯的。其原因是為了避免匯入過多未使用的包,從而導致編譯時間顯著增加。將 geometry.go
中的程式碼替換為如下程式碼:
// geometry.go
package main
import (
"geometry/rectangle" // 匯入自定的包
)
func main() {
}
上面的程式將會丟擲錯誤 geometry.go:6: imported and not used: "geometry/rectangle"
。
然而,在程式開發的活躍階段,又常常會先匯入包,而暫不使用它。遇到這種情況就可以使用空白識別符號 _
。
下面的程式碼可以避免上述程式的錯誤:
package main
import (
"geometry/rectangle"
)
var _ = rectangle.Area // 錯誤遮蔽器
func main() {
}
var _ = rectangle.Area
這一行遮蔽了錯誤。我們應該瞭解這些錯誤遮蔽器(Error Silencer)的動態,在程式開發結束時就移除它們,包括那些還沒有使用過的包。由此建議在 import 語句下面的包級別範圍中寫上錯誤遮蔽器。
有時候我們匯入一個包,只是為了確保它進行了初始化,而無需使用包中的任何函式或變數。例如,我們或許需要確保呼叫了 rectangle 包的 init 函式,而不需要在程式碼中使用它。這種情況也可以使用空白識別符號,如下所示。
package main
import (
_ "geometry/rectangle"
)
func main() {
}
執行上面的程式,會輸出 rectangle package initialized
。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。