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)
}

基本特性

  1. 對於同⼀個通道,發送操作之間是互斥的,接收操作之間也是互斥的。

在同⼀時刻,Go語⾔的運⾏時系統(以下簡稱運⾏時系統)只會執⾏對同⼀個通道的任意個發 送操作中的某⼀個。直到這個元素值被完全復制進該通道之后,其他針對該通道的發送操作才可能被執⾏。

類似的,在同⼀時刻,運⾏時系統也只會執⾏,對同⼀個通道的任意個接收操作中的某⼀個。

另外,對於通道中的同⼀個元素值來說,發送操作和接收操作之間也是互斥的。例如,雖然會出現,正在被復制進通道但還未 復制完成的元素值,但是這時它絕不會被想接收它的⼀⽅看到和取⾛。

需要注意的是:進⼊通道的並不是在接收操作符右邊的那個元素 值,⽽是它的副本

  1. 發送操作和接收操作中對元素值的處理都是不可分割的。
    如發送操作要么還沒復制元素值,要么已經復制完畢,絕不會出現只復制了⼀部分的情況。

  2. 發送操作在完全完成之前會被阻塞。接收操作也是如此。
    發送操作包括了“復制元素值”和“放置副本到通道內部”這兩個步驟。

在這兩個步驟完全完成之前,發起這個發送操作的那句代碼會⼀直阻塞在那⾥。也就是說,在它之后的代碼不會有執⾏的機 會,直到這句代碼的阻塞解除。

⻓時間的阻塞

  1. 緩沖通道
    如果通道已滿,那么對它的所有發送操作都會被阻塞,直到通道中有元素值被接收⾛。

由於發送操作在這種情況下被阻塞后,它們所在的goroutine會順序地進⼊通道內部的發送等待隊列,所以通知的順序總是公平的。

	// 示例1。
	ch1 := make(chan int, 1)
	ch1 <- 1
	//ch1 <- 2 // 通道已滿,因此這里會造成阻塞。

	// 示例2。
	ch2 := make(chan int, 1)
	//elem, ok := <-ch2 // 通道已空,因此這里會造成阻塞。
	//_, _ = elem, ok
	ch2 <- 1
  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語句中,需要注意:

  1. 如果像上述示例那樣加⼊了默認分⽀,那么⽆論涉及通道操作的表達式是否有阻塞,select語句都不會被阻塞。如果那 ⼏個表達式都阻塞了,或者說都沒有滿⾜求值的條件,那么默認分⽀就會被選中並執⾏。
  2. 如果沒有加⼊默認分⽀,那么⼀旦所有的case表達式都沒有滿⾜求值條件,那么select語句就會被阻塞。直到⾄少有⼀ 個case表達式滿⾜條件為⽌。
  3. select語句只能對其中的每⼀個case表達式各求值⼀次。
  4. select語句包含的候選分⽀中的case表達式都會在該語句執⾏開始時先被求值,並且求值的順序是依從代碼編寫的順序 從上到下的。
  5. 對於每⼀個case表達式,如果其中的發送表達式或者接收表達式在被求值時,相應的操作正處於阻塞狀態,那么對 該case表達式的求值就是不成功的。
  6. 如果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)
}

參數值在函數中傳遞

分為兩種類型來處理,值類型和引用類型

  1. 值類型
    所有傳給函數的參數值都會被復制,函數在其內部使⽤的並不是參數值的原 值,⽽是它的副本。

如下:

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函數中修改的只是原數組的副本⽽已, 並不會對原數組造成任何影響。

  1. 引用類型
    對於引⽤類型,⽐如:切⽚、字典、通道,像上⾯那樣復制它們的值,只會拷⻉它們本身⽽已,並不會拷⻉它們引⽤的 底層數據。也就是說,這時只是淺表復制,⽽不是深層復制。

以切⽚值為例,如此復制的時候,只是拷⻉了它指向底層數組中某⼀個元素的指針,以及它的⻓度值和容量值,⽽它的底層數 組並不會被拷⻉。

如下:

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傳入的是一個指針的引用,所以當指針所指向的底層數組發生變化,那么原值就會發生變化。

  1. 引用類型和值類型結合的類型

如下:

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"}新建了一個數組則不會改變。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM