上一章我們有提到了 Go 的變數宣告以及迴圈的用法,今天要來繼續介紹 Go 的基本語法:陣列、函式、struct以及指標。
這個部分一樣會用簡單的範例讓大家理解用法和需要注意的地方。
終於來到陣列系列了!我自己覺得這部分的宣告是蠻不一樣的~ 不知道大家是不是也這樣覺得 👀
好!那我們先從陣列開始~
Go 的陣列宣告需要一開始就指定長度,而且宣告後就不能再改變。以下是陣列宣告的 3 步驟:
// 宣告方式
var array_name [length]datatype{values}
array_name := [length]datatype{values}
// 範例
var numbers [5]int{1, 2, 3, 4, 5}
names := [3]string{"Apple", "Banana", "Candy"}
// 自己計算長度
var numbers [...]int{1, 2, 3, 4, 5} // 自動根據內容將長度設為 5
names := [...]string{"Apple", "Banana", "Candy"} // 自動根據內容將長度設為 3
再來如果要取得陣列長度的話,可以使用 len()
,例如:len(numbers)
,就會取得 numbers 陣列的長度 5。
一開始的時候有提到不能更改陣列的長度,但陣列的內容是可以修改的!
從下面的範例可以看到我們用 Peach 取代了原本第 0 個位置的 Apple。
// 範例
names := [...]string{"Apple", "Banana", "Candy"}
names[0] = "Peach"
fmt.Println(names[0]) // 輸出:Peach
再來是 slice 的部分。slice 的長度是可以改變的!主要由以下 3 種結構組合成的:
len()
函數取得。cap()
函數取得。首先,先來看 slice 的建立方式:
// 範例
// 空 slice(nil slice)
var s []int // 長度=0, 容量=0
// 1. 包含預設元素
nums := []int{1, 2, 3} // 長度=3, 容量=3
fmt.Println(nums) // 輸出:[1 2 3]
// 2. 使用 make()
nums1 := make([]int, 3, 5) // 長度=3, 容量=5
fmt.Println(nums1) // 輸出:[0 0 0]
// 3. 從 array 分割
arr := [5]int{10, 20, 30, 40, 50}
nums2 := arr[1:4] // 包含 arr[1], arr[2], arr[3]
fmt.Println(nums2) // 輸出:[20 30 40]
fmt.Println(len(nums2)) // 長度=3
fmt.Println(cap(nums2)) // 容量=4 (因為從 arr[1] 到 arr[4] 總共有 4 個空間)
再來是更改元素以及新增元素:
// 範例
// 更改元素
arr := [5]int{1, 2, 3, 4, 5} // 建立一長度 5 的陣列
num4 := arr[1:4] // 建立一 slice num4,包含 arr[1], arr[2], arr[3]
num4[0] = 99 // 改變 num4 的元素 → [99 3 4]
fmt.Println(arr) // 輸出:[1 99 3 4 5] → 底層 array 也被改變
// 新增元素
nums5 := []int{1, 2, 3} // 建立一 slice nums5(len=3, cap=3)
nums5 = append(nums5, 4, 5) // 新增 4, 5
fmt.Println(nums5) // 輸出:[1 2 3 4 5]
👉 當我們使用 append 要新增元素時,發現原本的容量不足,根本裝不下的話,就會重新分配「新的更大 array 」,指標也會改指向新的 array。
上述這樣的新增修改看起來很正常,不過要特別注意,因為 slice 是指向陣列,所以假設今天我們宣告了很多個 slice,並且都有修改元素的話,那被指向的那個陣列也會同時被修改唷!
來看一下範例:
// 範例
arr := [4]int{1, 2, 3, 4} // 陣列 [1 2 3 4]
a := arr[0:2] // 從陣列切出來的 slice a [1 2]
b := arr[1:3] // 從陣列切出來的 slice b [2 3]
b[0] = 99 // 改變 b[0] (其實就是 arr[1])
fmt.Println(a) // 輸出:[1 99]
fmt.Println(arr) // 輸出:[1 99 3 4]
從上面的這個範例就可以知道,slice 並不是 array!但是它指向 array。
所以當多個 slice 指向同一個 array 的時候,只要更改其中一個,其他的也會受到影響。
👉 常用切片用法:
s[:]
:建立一個引用整個slice 的新slice。s[low:high]
:創建一個從索引 low
到 high-1
的新slice,不包含 high
索引的元素。s[low:]
:從 low
開始到 slice 結尾。s[:high]
:從開使到 high-1
。s[:0]
:清空slice,將slice 的長度設為 0。cap()
到底是怎麼計算的?→ slice 的 容量 cap()
是從「切片起點 (index=1)」到「底層陣列結尾 (index=4)」之間,能夠使用的 最大 空間。
我們來看範例:
// 範例
arr := [5]int{10, 20, 30, 40, 50}
num := arr[1:4] // 包含 arr[1], arr[2], arr[3]
len(num) = 3 // 長度 3
cap(num) = 4 // 容量 4
// 為什麼容量是 4?
arr: [10] [20] [30] [40] [50]
idx: 0 1 2 3 4
num := arr[1:4] // [20] [30] [40]
len(num) = 3 // (位置 1 到 3,共 3 個元素)
cap(num) = 4 // (位置 1 到 4,最多能容納 4 個元素)
由此可知,雖然只切了 3 個元素,但是因為底層的 array 空間是 5,所以 num
雖然只切了 [20 30 40]
,但其實還有一個原本放 50 的空間([20 30 40 _]
)。
大家可以在自己的編譯器裡面用新增元素 append()
跑跑看 👇 就會知道它還有一個位子!
num = append(num, 99) // 在 [20,30,40] 後面加一個
fmt.Println(num) // [20 30 40 99]
所以:
len()
= end - start (實際切出來的元素數量)。cap()
= len(指向底層陣列) - start(從起始位置到陣列結尾的空間)。Go 的函式可以:
add
, addFunction
。👉 func [函式名](輸入值名稱 輸入值型態) 輸出值型態 {}
好的,我們來看範例:
// 範例
package main
import "fmt"
func noInput() string { // 沒有輸入值
return "沒有輸入值"
}
func input(input string) { // 只輸入不回傳
fmt.Println("只輸入不回傳", input)
}
func add(a int, b int) int { // 多個輸入值
return a + b
}
func main() {
res := noInput()
input("Hi") // 輸出:只輸入不回傳 Hi
sum := add(3, 5)
fmt.Println(res) // 輸出:沒有輸入值
fmt.Println(sum) // 輸出:8
}
// 範例
package main
import "fmt"
func noOutput() { // 沒有回傳值
fmt.Println("沒有回傳值")
}
func cal(a int, b int) (int, int) { // 多個回傳值
return a + b, a - b
}
func main() {
noOutput() // 輸出:沒有回傳值
sum, devide := cal(3, 5)
fmt.Println(sum, devide) // 輸出:8 -2
}
如果要將回傳值命名,可以這樣做:
// 範例
package main
import "fmt"
func cal(a int, b int) (sum int, devide int) { // 命名回傳值 sum 和 devide
sum = a + b
devide = a - b
return
}
func main() {
sum_res, deived_res := cal(3, 5) // 接回傳值
fmt.Println(sum_res, deived_res) // 輸出:8 -2
}
Go 的結構是自定義的類型,所以可以自行組合成自己想要的資料、方法。
可以把它想像成蓋房子,當今天我們想要快速地蓋好一棟房子時,會需要先把蓋房子的流程定義好,這樣開始製作的時候就可以呼叫定義好的 A、B、C 來幫助我們快速建造!
有了初步的想像之後,我們就來看範例吧!
// 範例
package main
import "fmt"
type Person struct { // 宣告 struct 結構
Name string
Age int
}
func main() {
p1 := Person{Name: "Alice", Age: 25} // 建立 struct
p2 := Person{"Bob", 30} // 建立的位置順序要跟宣告一致
fmt.Println(p1) // 輸出:{Alice 25}
fmt.Println(p2) // 輸出:{Bob 30}
p3 := Person{Name: "Charlie", Age: 20}
fmt.Println(p3.Name) // 輸出:Charlie
p3.Age = 21 // 更改 Age 為 21
fmt.Println(p3.Age) // 輸出:21
}
以上就是建立 struct 的用法~ 那…如果我有兩個 struct 是有關聯的那要怎麼做呢?
那我們就直接把它放進去吧!這部分 Go 是有支援的。我們用下面的例子來看看:
// 範例
package main
import "fmt"
type Person struct { // 宣告 struct 結構
Name string
Age int
Address // 直接嵌入
}
type Address struct {
City string
}
func main() {
data := Person{Name: "Eva", Age: 20, Address: Address{City: "Taipei"}}
fmt.Println(data.City) // 輸出:Taipei
fmt.Println(data) // 輸出:{Eva 20 {Taipei}}
}
👉 從上面的例子可以看見,data
是可以直接存取 Person
裡面 Address
的 City
資料的!
這樣的機制可以做很多設計唷!是不是很方便呀~
除了可以內嵌入之外,還可以搭配 function 唷!
我們就延續上面的範例來解釋:
// 範例
package main
import "fmt"
type Person struct { // 宣告 struct 結構
Name string
Year int
Address
}
type Address struct {
City string
}
func changeAge(a *Person) { // 直接傳入 struct
a.Year = a.Year + 1911
}
func (p Person)addName() string{ // 接收輸入的值,不會更改原本 struct 內容
return p.Name + "_"
}
func (p *Person)changeName(s string){ // 用指標的方式,會更改值
p.Name = s
}
func main() {
data := Person{Name: "Eva", Year: 80, Address: Address{City: "Taipei"}}
changeAge(&data) // 取得 data 的位址
fmt.Println(data.City) // 輸出:Taipei
fmt.Println(data) // 輸出:{Eva 1991 {Taipei}}
fmt.Println(data.addName()) // 輸出:Eva_
data.changeName("Ben") // 輸入 Ben
fmt.Println(data) // 輸出:{Ben 1991 {Taipei}}
}
從上面的例子可以看到, struct 和函式搭配可以變幻出很多組合,不過對於剛接觸的人來說,也是很容易搞混的~
要特別注意的就是,有沒有 指標 會很直接的影響結果。
Go 有提供 encoding/json
的 library 來處理 struct ↔ JSON 的轉換。
所對應到的 json 資料需要用反引號 ```` 將包起來(json:"name"
),我們看下面的範例:
// 範例 JSON → struct
package main
import (
"encoding/json" // 引入 library
"fmt"
)
type User struct {
Name string `json:"name"` // 標記對應的 JSON 欄位
Age int `json:"age"`
}
func main() {
data := `{"name": "Alice", "age": 25}`
var u User
err := json.Unmarshal([]byte(data), &u) // 把解析的結果放進 u,要記得加上 & 才會指向原本 User的位址
if err != nil { // 檢查解析結果,成功為 nil;失敗則印出錯誤
fmt.Println("Error:", err)
return
}
fmt.Println(u.Name, u.Age) // 輸出:Alice 25
}
再來看看把 struct 轉成 JSON 的範例:
// 範例 struct → JSON
package main
import (
"encoding/json" // 引入 library
"fmt"
)
type User struct {
Name string `json:"name"` // 標記對應的 JSON 欄位
Age int `json:"age"`
}
func main() {
u := User{Name: "Bob", Age: 30}
jsonData, err := json.Marshal(u) // 轉換並回傳
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(jsonData)) // 輸出: {"name":"Bob","age":30}
}
終於到最後了~
指標的用法在剛剛前面介紹的範例中也出現了一兩次,現在終於要來講它的基本用法了。
大概有這兩種用法:
&
:取得變數記憶體位址。*
:取出(或修改)該位址存的值。那就來看看這個簡單的基本範例吧:
// 範例
package main
import "fmt"
func main() {
x := 10
p := &x // p 存的是 x 的記憶體位址
fmt.Println("x =", x) // 輸出:x = 10
fmt.Println("p =", p) // 輸出:p = 0xc0000100b8(記憶體位址)
fmt.Println("*p =", *p) // 輸出:*p = 10(透過指標取得 x 的值)
*p = 20 // 修改指標指向的值 x
fmt.Println("x after *p=20:", x) // 輸出:x after *p=20: 20
}
這樣應該印出來就很清楚指標的概念了吧!
Go 的基本語法 part 2 就到這邊結束啦~