【轉】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"四個元素。然後我們定義了兩個切片,a
和b
,根據定義可以知道,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
來源:掘金
著作權歸原作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。