Golang流程和函數
這小節我們要介紹Go裏面的流程控制以及函數操作。
流程控制
流程控制在編程語言中是最偉大的發明了,因爲有了它,你可以通過很簡單的流程描述來表達很複雜的邏輯。Go中流程控制分三大類:條件判斷,循環控制和無條件跳轉。
if
if
也許是各種編程語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
Go裏面if
條件判斷語句中不需要括號,如下代碼所示
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
Go的if
還有一個強大的地方就是條件判斷語句裏面允許聲明一個變量,這個變量的作用域只能在該條件邏輯塊內,其他地方就不起作用了,如下所示
// 計算獲取值x,然後根據x返回的大小,判斷是否大於10。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
//這個地方如果這樣調用就編譯出錯了,因爲x是條件裏面的變量
fmt.Println(x)
多個條件的時候如下所示:
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
goto
Go有goto
語句——請明智地使用它。用goto
跳轉到必須在當前函數內定義的標籤。例如假設這樣一個循環:
func myFunc() {
i := 0
Here: //這行的第一個詞,以冒號結束作爲標籤
println(i)
i++
goto Here //跳轉到Here去
}
標籤名是大小寫敏感的。
for
Go裏面最強大的一個控制邏輯就是for
,它既可以用來循環讀取數據,又可以當作while
來控制邏輯,還能迭代操作。它的語法如下:
for expression1; expression2; expression3 {
//...
}
expression1
、expression2
和expression3
都是表達式,其中expression1
和expression3
是變量聲明或者函數調用返回值之類的,expression2
是用來條件判斷,expression1
在循環開始之前調用,expression3
在每輪循環結束之時調用。
一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:
package main
import "fmt"
func main(){
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
}
// 輸出:sum is equal to 45
有些時候需要進行多個賦值操作,由於Go裏面沒有,
操作符,那麼可以使用平行賦值i, j = i+1, j-1
有些時候如果我們忽略expression1
和expression3
:
sum := 1
for ; sum < 1000; {
sum += sum
}
其中;
也可以省略,那麼就變成如下的代碼了,是不是似曾相識?對,這就是while
的功能。
sum := 1
for sum < 1000 {
sum += sum
}
在循環裏面有兩個關鍵操作break
和continue
,break
操作是跳出當前循環,continue
是跳過本次循環。當嵌套過深的時候,break
可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:
for index := 10; index>0; index-- {
if index == 5{
break // 或者continue
}
fmt.Println(index)
}
// break打印出來10、9、8、7、6
// continue打印出來10、9、8、7、6、4、3、2、1
break
和continue
還可以跟着標號,用來跳到多重循環中的外層循環
for
配合range
可以用於讀取slice
和map
的數據:
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
由於 Go 支持 “多值返回”, 而對於“聲明而未被調用”的變量, 編譯器會報錯, 在這種情況下, 可以使用_
來丟棄不需要的返回值
例如
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
有些時候你需要寫很多的if-else
來實現一些邏輯處理,這個時候代碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候switch
就能很好的解決這個問題。它的語法如下
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
sExpr
和expr1
、expr2
、expr3
的類型必須一致。Go的switch
非常靈活,表達式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch
沒有表達式,它會匹配true
。
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
在第5行中,我們把很多值聚合在了一個case
裏面,同時,Go裏面switch
默認相當於每個case
最後帶有break
,匹配成功後不會自動向下執行其他case,而是跳出整個switch
, 但是可以使用fallthrough
強制執行後面的case代碼。
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
上面的程序將輸出
The integer was <= 6
The integer was <= 7
The integer was <= 8
default case
函數
函數是Go裏面的核心設計,它通過關鍵字func
來聲明,它的格式如下:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//這裏是處理邏輯代碼
//返回多個值
return value1, value2
}
上面的代碼我們看出
- 關鍵字
func
用來聲明一個函數funcName
- 函數可以有一個或者多個參數,每個參數後面帶有類型,通過
,
分隔 - 函數可以返回多個值
- 上面返回值聲明瞭兩個變量
output1
和output2
,如果你不想聲明也可以,直接就兩個類型 - 如果只有一個返回值且不聲明返回值變量,那麼你可以省略 包括返回值 的括號
- 如果沒有返回值,那麼就直接省略最後的返回信息
- 如果有返回值, 那麼必須在函數的外層添加return語句
下面我們來看一個實際應用函數的例子(用來計算Max值)
package main
import "fmt"
// 返回a、b中最大值.
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) //調用函數max(x, y)
max_xz := max(x, z) //調用函數max(x, z)
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在這直接調用它
}
上面這個裏面我們可以看到max
函數有兩個參數,它們的類型都是int
,那麼第一個變量的類型可以省略(即 a,b int,而非 a int, b int),默認爲離它最近的類型,同理多於2個同類型的變量或者返回值。同時我們注意到它的返回值就是一個類型,這個就是省略寫法。
多個返回值
Go語言比C更先進的特性,其中一點就是函數能夠返回多個值。
我們直接上代碼看例子
package main
import "fmt"
//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xTIMESy := SumAndProduct(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}
上面的例子我們可以看到直接返回了兩個參數,當然我們也可以命名返回參數的變量,這個例子裏面只是用了兩個類型,我們也可以改成如下這樣的定義,然後返回的時候不用帶上變量名,因爲直接在函數裏面初始化了。但如果你的函數是導出的(首字母大寫),官方建議:最好命名返回值,因爲不命名返回值,雖然使得代碼更加簡潔了,但是會造成生成的文檔可讀性差。
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
變參
Go函數支持變參。接受變參的函數是有着不定數量的參數的。爲了做到這點,首先需要定義函數使其接受變參:
func myfunc(arg ...int) {}
arg ...int
告訴Go這個函數接受不定數量的參數。注意,這些參數的類型全部是int
。在函數體中,變量arg
是一個int
的slice
:
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
傳值與傳指針
當我們傳一個參數值到被調用函數裏面時,實際上是傳了這個值的一份copy,當在被調用函數中修改參數值的時候,調用函數中相應實參不會發生任何變化,因爲數值變化只作用在copy上。
爲了驗證我們上面的說法,我們來看一個例子
package main
import "fmt"
//簡單的一個函數,實現了參數+1的操作
func add1(a int) int {
a = a+1 // 我們改變了a的值
return a //返回一個新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 應該輸出 "x = 3"
x1 := add1(x) //調用add1(x)
fmt.Println("x+1 = ", x1) // 應該輸出"x+1 = 4"
fmt.Println("x = ", x) // 應該輸出"x = 3"
}
看到了嗎?雖然我們調用了add1
函數,並且在add1
中執行a = a+1
操作,但是上面例子中x
變量的值沒有發生變化
理由很簡單:因爲當我們調用add1
的時候,add1
接收的參數其實是x
的copy,而不是x
本身。
那你也許會問了,如果真的需要傳這個x
本身,該怎麼辦呢?
這就牽扯到了所謂的指針。我們知道,變量在內存中是存放於一定地址上的,修改變量實際是修改變量地址處的內存。只有add1
函數知道x
變量所在的地址,才能修改x
變量的值。所以我們需要將x
所在地址&x
傳入函數,並將函數的參數的類型由int
改爲*int
,即改爲指針類型,才能在函數中修改x
變量的值。此時參數仍然是按copy傳遞的,只是copy的是一個指針。請看下面的例子
package main
import "fmt"
//簡單的一個函數,實現了參數+1的操作
func add1(a *int) int { // 請注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 應該輸出 "x = 3"
x1 := add1(&x) // 調用 add1(&x) 傳x的地址
fmt.Println("x+1 = ", x1) // 應該輸出 "x+1 = 4"
fmt.Println("x = ", x) // 應該輸出 "x = 4"
}
這樣,我們就達到了修改x
的目的。那麼到底傳指針有什麼好處呢?
- 傳指針使得多個函數能操作同一個對象。
- 傳指針比較輕量級 (8bytes),只是傳內存地址,我們可以用指針傳遞體積大的結構體。如果用參數值傳遞的話, 在每次copy上面就會花費相對較多的系統開銷(內存和時間)。所以當你要傳遞大的結構體的時候,用指針是一個明智的選擇。
- Go語言中
channel
,slice
,map
這三種類型的實現機制類似指針,所以可以直接傳遞,而不用取地址後傳遞指針。(注:若函數需改變slice
的長度,則仍需要取地址傳遞指針)
defer
Go語言中有種不錯的設計,即延遲(defer)語句,你可以在函數中添加多個defer語句。當函數執行到最後時,這些defer語句會按照逆序執行,最後該函數返回。特別是當你在進行一些打開資源的操作時,遇到錯誤需要提前返回,在返回前你需要關閉相應的資源,不然很容易造成資源泄露等問題。如下代碼所示,我們一般寫打開一個資源是這樣操作的:
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
我們看到上面有很多重複的代碼,Go的defer
有效解決了這個問題。使用它後,不但代碼量減少了很多,而且程序變得更優雅。在defer
後指定的函數會在函數退出前調用。
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
如果有很多調用defer
,那麼defer
是採用後進先出模式,所以如下代碼會輸出4 3 2 1 0
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
函數作爲值、類型
在Go中函數也是一種變量,我們可以通過type
來定義它,它的類型就是所有擁有相同的參數,相同的返回值的一種類型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
函數作爲類型到底有什麼好處呢?那就是可以把這個類型的函數當做值來傳遞,請看下面的例子
package main
import "fmt"
type testInt func(int) bool // 聲明瞭一個函數類型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 聲明的函數類型在這個地方當做了一個參數
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函數當做值來傳遞了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函數當做值來傳遞了
fmt.Println("Even elements of slice are: ", even)
}
函數當做值和類型在我們寫一些通用接口的時候非常有用,通過上面例子我們看到testInt
這個類型是一個函數類型,然後兩個filter
函數的參數和返回值與testInt
類型是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程序變得非常的靈活。
Panic和Recover
Go沒有像Java那樣的異常機制,它不能拋出異常,而是使用了panic
和recover
機制。一定要記住,你應當把它作爲最後的手段來使用,也就是說,你的代碼中應當沒有,或者很少有panic
的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?
Panic
是一個內建函數,可以中斷原有的控制流程,進入一個
panic
狀態中。當函數F
調用panic
,函數F的執行被中斷,但是F
中的延遲函數會正常執行,然後F返回到調用它的地方。在調用的地方,F
的行爲就像調用了panic
。這一過程繼續向上,直到發生panic
的goroutine
中所有調用的函數返回,此時程序退出。panic
可以直接調用panic
產生。也可以由運行時錯誤產生,例如訪問越界的數組。
Recover
是一個內建的函數,可以讓進入
panic
狀態的goroutine
恢復過來。recover
僅在延遲函數中有效。在正常的執行過程中,調用recover
會返回nil
,並且沒有其它任何效果。如果當前的goroutine
陷入panic
狀態,調用recover
可以捕獲到panic
的輸入值,並且恢復正常的執行。
下面這個函數演示瞭如何在過程中使用panic
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
下面這個函數檢查作爲其參數的函數在執行時是否會產生panic
:
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //執行函數f,如果f中出現了panic,那麼就可以恢復回來
return
}
main
函數和init
函數
Go裏面有兩個保留的函數:init
函數(能夠應用於所有的package
)和main
函數(只能應用於package main
)。這兩個函數在定義時不能有任何的參數和返回值。雖然一個package
裏面可以寫任意多個init
函數,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議用戶在一個package
中每個文件只寫一個init
函數。
Go程序會自動調用init()
和main()
,所以你不需要在任何地方調用這兩個函數。每個package
中的init
函數都是可選的,但package main
就必須包含一個main
函數。
程序的初始化和執行都起始於main
包。如果main
包還導入了其它的包,那麼就會在編譯時將它們依次導入。有時一個包會被多個包同時導入,那麼它只會被導入一次(例如很多包可能都會用到fmt
包,但它只會被導入一次,因爲沒有必要導入多次)。當一個包被導入時,如果該包還導入了其它的包,那麼會先將其它包導入進來,然後再對這些包中的包級常量和變量進行初始化,接着執行init
函數(如果有的話),依次類推。等所有被導入的包都加載完畢了,就會開始對main
包中的包級常量和變量進行初始化,然後執行main
包中的init
函數(如果存在的話),最後執行main
函數。下圖詳細地解釋了整個執行過程:
圖2.6 main函數引入包初始化流程圖
import
我們在寫Go代碼的時候經常用到import這個命令用來導入包文件,而我們經常看到的方式參考如下:
import(
"fmt"
)
然後我們代碼裏面可以通過如下的方式調用
fmt.Println("hello world")
上面這個fmt是Go語言的標準庫,其實是去GOROOT
環境變量指定目錄下去加載該模塊,當然Go的import還支持如下兩種方式來加載自己寫的模塊:
-
相對路徑
import “./model” //當前文件同一目錄的model目錄,但是不建議這種方式來import
-
絕對路徑
import “shorturl/model” //加載gopath/src/shorturl/model模塊
上面展示了一些import常用的幾種方式,但是還有一些特殊的import,讓很多新手很費解,下面我們來一一講解一下到底是怎麼一回事
-
點操作
我們有時候會看到如下的方式導入包
import( . "fmt" )
這個點操作的含義就是這個包導入之後在你調用這個包的函數時,你可以省略前綴的包名,也就是前面你調用的fmt.Println("hello world")可以省略的寫成Println("hello world")
-
別名操作
別名操作顧名思義我們可以把包命名成另一個我們用起來容易記憶的名字
import( f "fmt" )
別名操作的話調用包函數時前綴變成了我們的前綴,即f.Println("hello world")
-
_操作
這個操作經常是讓很多人費解的一個操作符,請看下面這個import
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作其實是引入該包,而不直接使用包裏面的函數,而是調用了該包裏面的init函數。