Java程式設計師學習Go語言—之一
轉載:https://www.luozhiyun.com/archives/206
GOPATH 工作空間
GOPATH簡單理解成Go語言的工作目錄,它的值是一個目錄的路徑,也可以是多個目錄路徑,每個目錄都代表Go語言的一個工作區(workspace)。
在GOPATH放置Go語言的原始碼檔案(source file),以及安裝(install)後的歸檔檔案(archive file,也就是以“.a”為副檔名的檔案)和可執行檔案(executable file)。
原始碼安裝
比如,一個已存在的程式碼包的匯入路徑是
github.com/labstack/echo,
那麼執行命令進行原始碼的安裝
go install github.com/labstack/echo
在安裝後如果產生了歸檔檔案(以“.a”為副檔名的檔案),就會放進該工作區的pkg子目錄;如果產生了可執行檔案,就可能會放進該工作區的bin子目錄。
上面該命令在安裝後生成的歸檔檔案的相對目錄就是 github.com/labstack, 檔名為echo.a。
除此之外,歸檔檔案的相對目錄與pkg目錄之間還有一級目錄,叫做平臺相關目錄。平臺相關目錄的名稱是由build(也稱“構建”)的目標作業系統、下劃線和目標計算架構的代號組成的。
比如,構建某個程式碼包時的目標作業系統是Linux,目標計算架構是64位的,那麼對應的平臺相關目錄就是linux_amd64。
程式碼塊中的重名變數
我們來看一下下面的程式碼:
var block = "package"
func main() {
block := "function"
{
block := "inner"
fmt.Printf("The block is %s.\n", block)
}
fmt.Printf("The block is %s.\n", block)
blockFun()
}
這個命令原始碼⽂件中有四個程式碼塊,它們是:全域程式碼塊、main包代表的程式碼塊、main函式代表的程式碼塊,以及在main函 數中的⼀個⽤花括號包起來的程式碼塊。
如果執行該程式碼,那麼會得到如下結果:
The block is inner.
The block is function.
在go中,首先,程式碼引⽤變數的時候總會最優先查詢當前程式碼塊中的那個變數。
其次,如果當前程式碼塊中沒有宣告以此為名的變數,那麼程式會沿著程式碼塊的巢狀關係,從直接包含當前程式碼塊的那個代 碼塊開始,⼀層⼀層地查詢。
⼀般情況下,程式會⼀直查到當前程式碼包代表的程式碼塊。如果仍然找不到,那麼Go語⾔的編譯器就會報錯了。
所以上面的例子中,main程式碼塊首先無法引用到最內層程式碼塊中的變數,最內層的程式碼塊也會優先去找自己程式碼塊的變數。
需要注意一點的是,在不同的程式碼塊中,變數的名字可以相同但是型別可以不同的。
其實如果使用過java,就會發現這些都和java的變數申明是一樣的。
變數的型別
判斷變數型別
在java中,我們可以用instanceof來判斷型別,在go中要稍微麻煩一點,具體的如下:
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
fmt.Printf("The element is %q.\n", container[1])
value2, ok2 := interface{}(container).(map[int]string)
value1, ok1 := interface{}(container).([]string)
fmt.Println(value1)
fmt.Println(value2)
if !(ok1 || ok2) {
fmt.Printf("Error: unsupported container type: %T\n", container)
return
}
}
也就是說需要通過interface{}(container).(map[int]string)
這樣的一句表示式來實現判斷型別。
它包括了⽤來把container變數的值轉換為空接⼝值的interface{}(container)。 以及⼀個⽤於判斷前者的型別是否為map型別 map[int]string 的 .(map[int]string)。
這個表示式返回兩個變數,ok代表是否判斷成功,如果為true,那麼被判斷的值將會被自動轉換為map[int]string,否則value將被賦 予nil(即“空”)。
強制型別轉換
我們一般可以通過如下的方式實現型別轉換:
var srcInt = int16(-255)
dstInt := int8(srcInt)
fmt.Println(dstInt)
在上面的型別轉換中需要注意的是,這裡是範圍大的型別轉換成範圍小的型別,Go語⾔會把在較⾼ 位置(或者說最左邊位置)上的8位⼆進位制數直接截掉,所以dstInt的值就是1。
類似的快⼑斬亂麻規則還有:當把⼀個浮點數型別的值轉換為整數型別值時,前者的⼩數部分會被全部截掉。
所以在型別轉換的時候要時刻提防類型範圍的問題。
類型別名和潛在型別
別名型別與其源型別的區別恐怕只是在名稱上,它們 是完全相同的。
type MyString = string
定義新的型別,這個型別會不同於其他任何型別。
type MyString2 string // 注意,這⾥沒有等號。
如果兩個值潛在型別相同,卻屬於不同型別,它們之間是可以進⾏型別轉換的。如下:
type MyString string
str := "BCD"
myStr1 := MyString(str)
myStr2 := MyString("A" + str)
但是兩個型別的潛在型別相同,它們的值之間也不能進⾏判等或⽐較,它們的變數之間也不能賦值。如下:
type MyString2 string
str := "BCD"
myStr2 := MyString2(str)
//myStr2 = str // 這裡的賦值不合法,會引發編譯錯誤。
//fmt.Printf("%T(%q) == %T(%q): %v\n",
// str, str, myStr2, myStr2, str == myStr2) // 這裡的判等不合法,會引發編譯錯誤。
對於集合類的型別[]MyString2與[]string來說是不可以進⾏型別轉換和比較的,因為[]MyString2與[]string的潛在型別不 同,分別是MyString2和string。如下:
type MyString string
strs := []string{"E", "F", "G"}
var myStrs []MyString
//myStrs := []MyString(strs) // 這裡的型別轉換不合法,會引發編譯錯誤。
管道channel
通道型別的值本身就是併發安全的,這也是Go語⾔⾃帶的、唯⼀⼀個可以滿⾜併發安全性的型別。
當容量為0時,我們可以稱通道為⾮緩衝通道,也就是不帶緩衝的通道。⽽當容量⼤於0時,我們可以稱為緩衝通道,也就是 帶有緩衝的通道。
⼀個通道相當於⼀個先進先出(FIFO)的佇列。也就是說,通道中的各個元素值都是嚴格地按照發送的順序排列的,先被髮 送通道的元素值⼀定會先被接收。元素值的傳送和接收都需要⽤到操作符<-。我們也可以叫它接送操作符。⼀個左尖括號緊 接著⼀個減號形象地代表了元素值的傳輸⽅向。
func main() {
ch1 := make(chan int, 3)
//往channel中放入元素
ch1 <- 2
ch1 <- 1
ch1 <- 3
//往channel中獲取元素
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",
elem1)
}
基本特性
- 對於同⼀個通道,傳送操作之間是互斥的,接收操作之間也是互斥的。
在同⼀時刻,Go語⾔的運⾏時系統(以下簡稱運⾏時系統)只會執⾏對同⼀個通道的任意個發 送操作中的某⼀個。直到這個元素值被完全複製進該通道之後,其他針對該通道的傳送操作才可能被執⾏。
類似的,在同⼀時刻,運⾏時系統也只會執⾏,對同⼀個通道的任意個接收操作中的某⼀個。
另外,對於通道中的同⼀個元素值來說,傳送操作和接收操作之間也是互斥的。例如,雖然會出現,正在被複制進通道但還未 複製完成的元素值,但是這時它絕不會被想接收它的⼀⽅看到和取⾛。
需要注意的是:進⼊通道的並不是在接收操作符右邊的那個元素 值,⽽是它的副本。
傳送操作和接收操作中對元素值的處理都是不可分割的。
如傳送操作要麼還沒複製元素值,要麼已經複製完畢,絕不會出現只複製了⼀部分的情況。傳送操作在完全完成之前會被阻塞。接收操作也是如此。
傳送操作包括了“複製元素值”和“放置副本到通道內部”這兩個步驟。
在這兩個步驟完全完成之前,發起這個傳送操作的那句程式碼會⼀直阻塞在那⾥。也就是說,在它之後的程式碼不會有執⾏的機 會,直到這句程式碼的阻塞解除。
⻓時間的阻塞
- 緩衝通道
如果通道已滿,那麼對它的所有傳送操作都會被阻塞,直到通道中有元素值被接收⾛。
由於傳送操作在這種情況下被阻塞後,它們所在的goroutine會順序地進⼊通道內部的傳送等待佇列,所以通知的順序總是公平的。
// 示例1。
ch1 := make(chan int, 1)
ch1 <- 1
//ch1 <- 2 // 通道已滿,因此這裡會造成阻塞。
// 示例2。
ch2 := make(chan int, 1)
//elem, ok := <-ch2 // 通道已空,因此這裡會造成阻塞。
//_, _ = elem, ok
ch2 <- 1
- ⾮緩衝通道
⽆論是傳送操作還是接收操作,⼀開始執⾏就會被阻塞,直到配對的操作也開始執⾏,才 會繼續傳遞。由此可⻅,⾮緩衝通道是在⽤同步的⽅式傳遞資料。也就是說,只有收發雙⽅對接上了,資料才會被傳遞。
ch1 := make(chan int )
ch1 <- 10
fmt.Println("End." )//這裡會造成阻塞。
關閉通道
對於⼀個已初始化的通道來說,如果通道一旦關閉,再對它進⾏傳送操作,就會 引發panic。
如果試圖關閉⼀個已經關閉了的通道,也會引發panic。
所以我們在關閉通道的時候應當讓傳送方做這件事,接收操作是可以感知到通道的關閉的,並能夠安全退出。
如果通道關閉時,⾥⾯還有元素值未被取出,那麼接收表示式的第⼀個結果,仍會是通道中的某⼀個元素值,⽽第⼆個 結果值⼀定會是true。
func main() {
ch1 := make(chan int, 2)
// 傳送方。
go func() {
for i := 0; i < 10; i++ {
fmt.Printf("Sender: sending element %v...\n", i)
ch1 <- i
}
fmt.Println("Sender: close the channel...")
close(ch1)
}()
// 接收方。
for {
elem, ok := <-ch1
if !ok {
fmt.Println("Receiver: closed channel")
break
}
fmt.Printf("Receiver: received an element: %v\n", elem)
}
fmt.Println("End.")
}
單向通道
如下,這表示了這個通道是單向的,並且只能發⽽不能收。
var uselessChan = make(chan<- int, 1)
單向通道最主要的⽤途就是約束其他程式碼的⾏為。
例如:
func main() {
// 初始化一個容量為3的通道
intChan1 := make(chan int, 3)
//將通道傳入到函式中
SendInt(intChan1)
}
//使用單向通道限制這個函式只能放入元素到通道中
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}
在SendInt函式中的程式碼只能 向引數ch傳送元素值,⽽不能從它那⾥接收元素值。這就起到了約束函式⾏為的作⽤。
同樣單通道也可以作為函式的返回值:
func main() {
intChan2 := getIntChan()
for elem := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem)
}
}
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}
函式getIntChan會返回⼀個<-chan int型別的通道,這就意味著得到該通道的程式,只能從通道中接收元素值。
select多路選擇
select語句與通道聯⽤
select語句只能與通道聯⽤,它⼀般由若⼲個分⽀組成。每次執⾏這種語句的時候,⼀般只有⼀個分⽀中的程式碼會被運⾏。
我們通過下面的例子來展示:
func example1() {
// 準備好幾個通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 隨機選擇一個通道,並向它傳送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一個通道中有可取的元素值,哪個對應的分支就會被執行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
}
在使用select語句中,需要注意:
- 如果像上述示例那樣加⼊了預設分⽀,那麼⽆論涉及通道操作的表示式是否有阻塞,select語句都不會被阻塞。如果那 ⼏個表示式都阻塞了,或者說都沒有滿⾜求值的條件,那麼預設分⽀就會被選中並執⾏。
- 如果沒有加⼊預設分⽀,那麼⼀旦所有的case表示式都沒有滿⾜求值條件,那麼select語句就會被阻塞。直到⾄少有⼀ 個case表示式滿⾜條件為⽌。
- select語句只能對其中的每⼀個case表示式各求值⼀次。
- select語句包含的候選分⽀中的case表示式都會在該語句執⾏開始時先被求值,並且求值的順序是依從程式碼編寫的順序 從上到下的。
- 對於每⼀個case表示式,如果其中的傳送表示式或者接收表示式在被求值時,相應的操作正處於阻塞狀態,那麼對 該case表示式的求值就是不成功的。
- 如果select語句發現同時有多個候選分⽀滿⾜選擇條件,那麼它就會⽤⼀種偽隨機的演算法在這些分⽀中選擇⼀個並執⾏。
超時控制
select 裡面會根據兩個case的返回時間來選擇執行,哪個先返回哪個就先執行,所以利用這個功能,可以實現超時返回。
func TestSelect(t *testing.T) {
//select 裡面會根據兩個case的返回時間來選擇執行
//哪個先返回哪個就先執行
//所以利用這個功能,可以實現超時返回
select {
case ret:=<-AsyncService():
t.Log(ret)
case <-time.After(time.Microsecond*100):
t.Error("time out")
}
}
func AsyncService() chan string {
retCh := make(chan string,1)
go func() {
ret := service()
fmt.Println("return result.")
retCh <- ret
fmt.Println("service exited.")
}()
return retCh
}
函式
接受其他的函式作為引數傳⼊
我們可以先申明一個函式型別:
type operate func(x, y int) int
然後將這個函式當做引數傳入到函式內
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
閉包
可以借閉包在程式運⾏的過程中,根據需要⽣成功能不同的函式,繼⽽影響後續的程式⾏為。
例如:
type calculateFunc func(x int, y int) (int, error)
func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}
func main() {
x, y = 56, 78
add := genCalculator(op)
result, err = add(x, y)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
}
引數值在函式中傳遞
分為兩種型別來處理,值型別和引用型別
- 值型別
所有傳給函式的引數值都會被複制,函式在其內部使⽤的並不是引數值的原 值,⽽是它的副本。
如下:
func main() {
// 示例1。
array1 := [3]string{"a", "b", "c"}
fmt.Printf("The array: %v\n", array1)
array2 := modifyArray(array1)
fmt.Printf("The modified array: %v\n", array2)
fmt.Printf("The original array: %v\n", array1)
fmt.Println()
}
// 示例1。
func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}
返回的是:
The array: [a b c]
The modified array: [a x c]
The original array: [a b c]
由於陣列是值型別,所以每⼀次複製都會拷⻉它,以及它的所有元素值。我在modify函式中修改的只是原陣列的副本⽽已, 並不會對原陣列造成任何影響。
- 引用型別
對於引⽤型別,⽐如:切⽚、字典、通道,像上⾯那樣複製它們的值,只會拷⻉它們本身⽽已,並不會拷⻉它們引⽤的 底層資料。也就是說,這時只是淺表複製,⽽不是深層複製。
以切⽚值為例,如此複製的時候,只是拷⻉了它指向底層陣列中某⼀個元素的指標,以及它的⻓度值和容量值,⽽它的底層數 組並不會被拷⻉。
如下:
func main() {
slice1 := []string{"x", "y", "z"}
fmt.Printf("The slice: %v\n", slice1)
slice2 := modifySlice(slice1)
fmt.Printf("The modified slice: %v\n", slice2)
fmt.Printf("The original slice: %v\n", slice1)
fmt.Println()
}
func modifySlice(a []string) []string {
a[1] = "i"
return a
}
返回:
The slice: [x y z]
The modified slice: [x i z]
The original slice: [x i z]
由於類modifySlice傳入的是一個指標的引用,所以當指標所指向的底層陣列發生變化,那麼原值就會發生變化。
- 引用型別和值型別結合的型別
如下:
func main() {
complexArray1 := [3][]string{
[]string{"d", "e", "f"},
[]string{"g", "h", "i"},
[]string{"j", "k", "l"},
}
fmt.Printf("The complex array: %v\n", complexArray1)
complexArray2 := modifyComplexArray(complexArray1)
fmt.Printf("The modified complex array: %v\n", complexArray2)
fmt.Printf("The original complex array: %v\n", complexArray1)
}
func modifyComplexArray(a [3][]string) [3][]string {
a[1][1] = "s"
a[2] = []string{"o", "p", "q"}
return a
}
返回:
The complex array: [[d e f] [g h i] [j k l]]
The modified complex array: [[d e f] [g s i] [o p q]]
The original complex array: [[d e f] [g s i] [j k l]]
實際上還是和上面的一樣的理論,傳入modifyComplexArray方法的陣列是複製的,但是數組裡面的元素傳的是引用,所以直接修改引用的切片值會影響到原來的值,但是直接以這樣的方式a[2] = []string{"o", "p", "q"}
新建了一個數組則不會改變