1. 程式人生 > 實用技巧 >【轉】Golang入門(1):一天學完GO的基本語法

【轉】Golang入門(1):一天學完GO的基本語法

摘要

在配置好環境之後,要研究的就是這個語言的語法了。在這篇文章中,作者希望可以簡單的介紹一下Golang的各種語法,並與C和Java作一些簡單的對比以加深記憶。因為這篇文章只是入門Golang的第二篇文章,所以本文並不會對一些指令進行深挖,僅僅只是停留在“怎麼用”的程度,至於“為什麼是這樣”,則涉及到了具體的應用場景和彙編指令,作者將會在以後的文章中進行介紹。

1 導包

總所周知,“Hello World”是程式設計師的一種儀式感。

而這一行“Hello World”,一定會涉及到輸入輸出相關的方法。所以,如何匯入包,是我們需要研究的第一步。

在C語言中,我們使用include,在Java中,我們使用了import。在Golang中也一樣,我們使用import引入其他的包。在上一篇文章中,我們已經提到了對於匯入的包,編譯器會首先在GOROOT中尋找,隨後會在專案所對應的GOPATH中尋找,最後才是在全域性GOPATH中尋找,如果都無法找到,編譯器將會報錯。

注意,在Golang中和Java有一點很大的區別,就是在Golang中,import匯入的是目錄,而不是包名。而且,Golang沒有強制要求包名和目錄名需要一致。

下面舉一些例子來說明在Golang中包名和目錄的關係,先來看看目錄結構:


可以看出,我們在src下面設定了兩個資料夾,在第二個資料夾下面設定了兩個go檔案。
來看看這兩個檔案的程式碼,test1.go如下:

package pktest

func Func1()  {
	println("這是第一個函式")
}
複製程式碼

test2.go如下:

package pktest

func Func2()  {
	println("這是第二個函式")
}
複製程式碼

然後我們再來看看testmain.go下面的內容:

package main

import "package1/package2"

func main() {
	pktest.Func1()
}
複製程式碼

注意到了嗎,我們在呼叫Func1這個函式的時候,使用的是pktest,而不是我們認為的package1/package2中的package2

按照我們在Java中的思想,我們應該是使用package2.Func1的呼叫方法或者說是使用test1.Func1這樣的方法。

這是因為在Golang中,沒有強制要求包名和目錄名稱一致。也就是說,在上面的例子中,我們引用路徑中的資料夾名稱是package2,而在這個資料夾下面的兩個檔案,他們的包名,卻被設定成了pktest。而在Golang的引用中,我們需要填寫的是原始檔所在的相對路徑

也就是說,我們可以理解為,包名和路徑其實是兩個概念,檔名在Golang中不會被顯式的引用,通常的引用格式是packageName.FunctionName

結論如下:

  • import匯入的是原始檔的相對路徑,而不是包名。
  • 在習慣上將包名和目錄名保證一致,但這並不是強制規定(但不建議這麼做,這樣容易造成呼叫這個包的人,無法快速知道這個包的名稱是什麼)
  • 在程式碼中引用包內的成員時,使用包名而不是目錄名。
  • 在一個資料夾內,只能存在一種包名,原始檔的名稱也沒有其他的限制。
  • 如果多個資料夾下有相同名字的package,它們其實是彼此無關的package。

以上部分內容摘自於這篇文章

2 宣告

看完了導包方面的內容,我們再來看看如何宣告一個變數。在宣告變數這一部分,和C以及Java也有較大的區別

2.1 變數的定義

我們先定義一些變數看看:

var a int
var b float32
var c, d float64
e, f := 9, 10
var g = "Ricardo"
複製程式碼

我們可以看到,在Golang中定義一個變數,需要使用var關鍵字,而與C或者Java不同的是,我們需要將這個變數的型別寫在變數名的後面。不僅如此,在Golang中,允許我們一次性定義多個變數並同時賦值。

還有另外的一種做法,是使用:=這個符號。使用了這個符號之後,開發者不再需要寫var關鍵字,只需要定義變數名,並在後面進行賦值即可。並且,Golang編譯器會根據後面的值的型別,自動推匯出變數的型別。

在變數的定義過程中,如果定義的時候就賦予了變數的初始值,是不需要再宣告變數的型別的,如變數g

注意,Golang是強型別的一種語言,所有的變數必須擁有型別,並且變數僅僅可以儲存特定型別的資料。

2.2 匿名變數

識別符號為_(下劃線)的變數,是系統保留的匿名變數,在賦值後,會被立即釋放,稱之為匿名變數。其作用是變數佔位符,對其變數賦值結構。通常會在批量賦值時使用。
例如,函式返回多個值,我們僅僅需要其中部分,則不需要的使用_來佔位

func main() {
  // 呼叫函式,僅僅需要第二個返回值,第一,三使用匿名變數佔位
  _, v, _ := getData()
  fmt.Println(v)
}
// 返回兩個值的函式
func getData() (int, int, int) {
  // 返回3個值
  return 2, 4, 8
}
複製程式碼

如上述程式碼所示,如果我僅僅需要一個變數的值,就不需要去額外定義一些沒有意義的變數名了,僅僅只是需要使用佔位符這種“用後即焚”的匿名變數。

2.3 常量

在Golang的常量定義中,使用const關鍵字,並且不能使用:=識別符號。

3 判斷

我們在使用Java或者C的時候,寫判斷語句是這樣的:

if(condition){
    ...
}
複製程式碼

在Golang中,唯一的不同是不需要小括號,但是大括號還是必須的。如下:

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
	    return v
	}
	return lim
}
複製程式碼

除去不需要寫小括號以外,Golang還允許在判斷條件之前執行一個簡單的語句,並用一個分號隔開。

4 迴圈

在Golang中,只有一種迴圈,for迴圈。

和判斷語句一樣,在Golang中也是沒有小括號的。

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}
複製程式碼

此外,在迴圈條件中,初始化語句和後置語句是可選的,這個時候把分號去掉,for迴圈就變成了while迴圈

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}
複製程式碼

不僅如此,如果省略迴圈條件,該迴圈就不會結束,因此無限迴圈可以寫得很緊湊,這個時候,和while(true)的效果是一樣的。

func main() {
	for {
	    ...
	}
}
複製程式碼

5 函式

5.1 函式的定義

在Golang的函式定義中,所有的函式都以func開頭,並且Golang命名推薦使用駝峰命名法。

注意,在Golang的函式中,如果首字母是小寫,則只能在包內使用;如果首字母是大寫,則可以在包外被引入使用。可以理解為,使用小寫的函式,是`private`的,使用大寫的函式,是`public`的。

在Golang的函式定義中,一樣可以不接受引數,或者接受多個引數。而在引數的定義過程中,也是按照定義變數的格式,先定義變數名,再宣告變數型別。對於函式的返回型別,也是按照這樣的格式,先寫函式名,再寫返回型別:

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}
複製程式碼

並且,對於相同型別的兩個引數,引數型別可以只寫一個,用法如下:

func add(x, y int) int {
	return x + y
}
複製程式碼

在Golang中,對於函式的返回值,和C以及Java是不一樣的。

Golang中的函式可以返回任意多個返回值。

例如下面的小李子,

func swap(x, y string) (string, string) {
	return y, x
}

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}
複製程式碼

其次,函式的返回值是可以被命名的:

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}
複製程式碼

在這裡,我們可以理解為在函式的頂部預先定義了這些變數值,而空的return語句則預設返回所有已經定義的返回變數。

5.2defer

在Golang中,有一個關鍵字叫defer

defer 語句會將函式推遲到外層函式返回之後執行。 推遲呼叫的函式其引數會立即求值,但直到外層函式返回前該函式都不會被呼叫。

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}
複製程式碼

在這段程式碼中,本來的執行路徑是從上往下,也就是先輸出“world”,然後再輸出“hello”。但是因為defer這個關鍵字的存在,這行語句將在最後才執行,所以產生了先列印“hello”然後再列印“world”的效果。

注意,defer後面必須是函式呼叫語句,不能是其他語句,否則編譯器會報錯。

可以考慮到的場景是,檔案的關閉,或資料庫連線的釋放等,這樣開啟和關閉的程式碼寫在一起,既可以使得程式碼更加的整潔,也可以防止出現開發者在寫了長長的業務程式碼後,忘記關閉的情況。

至於defer的底層實現,本文不進行詳細的解釋,簡單來講就是將defer語句後面的函式呼叫的地址壓進一個棧中,在當前的函式執行完畢,CPU即將執行函式外的下一行程式碼之前,先把棧中的指令地址彈出給CPU執行,直到棧為空,才結束這個函式,繼續執行後面的程式碼。

從上文剛剛的表述中也可以推斷出,如果有多條refer語句,將會從下往上依次執行。

因為本文只是對各種指令簡單的進行對比,所以對於refer的詳細解釋,將在以後的文章中詳細說明。

6 指標

對於指標,如果是C或者C++開發者,一定很熟悉;而對於Java開發者,指標是對開發者透明的一個東西,一個物件會在堆中佔據一定的記憶體空間,而在當前的棧楨中,有一個區域性變數,他的值就是那個物件的首地址,這也是一個指標。

可以說,指標就是開發者訪問記憶體的一種途徑,只不過是由控制權交給了開發者還是虛擬機器。

在Golang中,指標的用法和 C 是一樣的。同樣是用&取地址,用*取地址中的值。

但是,與 C 不同,Golang沒有指標運算。

7 陣列

在Golang中,陣列的定義是這樣的:

var a [10]int
複製程式碼

這樣做會將變數 a 宣告為擁有 10 個整數的陣列。

注意,在Golang中,陣列的大小也同樣和 C 語言一樣不能改變。

7.1切片

陣列的切片,顧名思義,就是將一個數組按需切出自己所需的部分。

每個陣列的大小都是固定的。而切片則為陣列元素提供動態大小的、靈活的視角。在實踐中,切片比陣列更常用。

切片通過兩個下標來界定,即一個上界和一個下界,二者以冒號分隔:

a[low : high]
複製程式碼

它會選擇一個半開區間,包括第一個元素,但排除最後一個元素。

以下表達式建立了一個切片,它包含 a 中下標從 1 到 3 的元素:

a[1:4]
複製程式碼

舉個例子:

func main() {
	str := [4]string{
	    "aaa",
	    "bbb",
	    "ccc",
	    "ddd",
	}
	fmt.Println(str)

	a := str[0:2]
	b := str[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(str)
}
複製程式碼

我們定義了一個數組,裡面含有"aaa","bbb","ccc","ddd"四個元素。然後我們定義了兩個切片,ab,根據定義可以知道,a為"aaa"和"bbb",b為"bbb"和"ccc"。

這個時候,我們把b[0]改成了"XXX",那麼b變成了"XXX"和"ccc",這是毋庸置疑的。但是與直覺相違背的是,這個時候的陣列str,也變成了"aaa","XXX","ccc","ddd"。

這是因為,Golang中的切片,不是拷貝,而是定義了新的指標,指向了原來陣列所在的記憶體空間。所以,修改了切片陣列的值,也就相應的修改了原陣列的值了。

此外,切片可以用append增加元素。但是,如果此時底層陣列容量不夠,此時切片將會指向一個重新分配空間後進行拷貝的陣列。

因此可以得出結論:

  • 切片並不儲存任何資料,它只是描述了底層陣列中的一段。
  • 更改切片的元素會修改其底層陣列中對應的元素。
  • 與它共享底層陣列的切片都會觀測到這些修改。

7.2 make

切片可以用內建函式 make 來建立,這也是你建立動態陣列的方式。

在此之前需要解釋兩個定義,len(長度)和cap(容量)。
len是陣列的長度,指的是這個陣列在定義的時候,所約定的長度。  
cap是陣列的容量,指的是底層陣列的長度,也可以說是原陣列在記憶體中的長度。
在前文中所提到的切片,如果我定義了一個str[0,0]的切片,此時的長度為0,但是容量依舊還是5。
複製程式碼

make 函式會分配一個元素為零值的陣列並返回一個引用了它的切片:

a := make([]int, 5)  // len(a)=5
複製程式碼

要指定它的容量,需向 make 傳入第三個引數:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4
複製程式碼

也就是說,make函式可以自定義切片的大小。用Java的話來說,他可以被過載。

有兩種形式,如果只有兩個引數,第一個引數是陣列內元素的型別,第二個引數是陣列的長度(此時長度和容量都為5)。

而如果有第三個引數,那麼第三個引數可以指定陣列的容量,即可以指定這個陣列在記憶體中分配多大的空間。

寫在最後

首先,謝謝你能看到這裡。

如果這篇文章對你能起到哪怕一點點的幫助,作者都會很開心!

其次要說明的是,我也是剛開始接觸Golang,寫這篇文章的目的是起到一個筆記的效果,能夠去比較一些C,Java,Golang中的語法區別,也一定會有不少的認知錯誤。如果在這篇文章中你看到了任何與你的認識有差距的地方,請一定指出作者的錯誤。如果本文有哪些地方是作者講的不夠明白的,或者是你不理解的,也同樣歡迎留言,一起交流學習進步。

而且在本文中,很多地方沒有進行深入挖掘,在這篇文章中,就只是單純的學會怎麼用,就達到目的了。深入的可以參考原作者的其他文章哦。

那麼在最後,再次感謝~

PS:如果有其他的問題,也可以在公眾號找到原作者。並且,所有文章第一時間會在公眾號更新


原作者:紅雞菌
連結:https://juejin.cn/post/6844904117450571790
來源:掘金
著作權歸原作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。