1. 程式人生 > >Go 系列教程-2 基礎知識

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 函式可用於執行初始化任務,也可用於在開始執行之前驗證程式的正確性。

包的初始化順序如下:

  1. 首先初始化包級別(Package Level)的變數
  2. 緊接著呼叫 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 做了如下修改:

  1. 變數 rectLen 和 rectWidth 從 main 函式級別移到了包級別。
  2. 添加了 init 函式。當 rectLen 或 rectWidth 小於 0 時,init 函式使用 log.Fatal 函式列印一條日誌,並終止了程式。

main 包的初始化順序為:

  1. 首先初始化被匯入的包。因此,首先初始化了 rectangle 包。
  2. 接著初始化了包級別的變數 rectLen 和 rectWidth
  3. 呼叫 init 函式。
  4. 最後呼叫 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。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。