惟理的 Go 進度報告

函式課已完善:完整 30 分鐘

第 17 章補齊時間表、作業 sub/avg 完整說明。每個程式碼框下都有「加了什麼/為什麼這樣寫」。

21 章可讀內容
hello → O 練習檔對照
公開網址 GitHub Pages
入門
hello.go
變數常數
A–C
輸出型別
D–G
陣列
H–I
Slice
J–K
條件迴圈
L–N
函式
O

關於這份報告

本手冊整理自「惟理」的 Go 學習過程:從第一次複習教學,到後來新增的 J.goO.go
寫作原則: 跟課堂講解一樣清楚,不刻意省略、不為了短而簡化。
更新約定: 之後每次學完新內容,都會把新章節補進這份手冊。

使用說明

  1. 1. 建議對照 repo 裡的練習檔案一起看(hello.goA.goO.go)。
  2. 2. 每一章末尾有「自測」——先自己答,再對答案。
  3. 3. 章節順序就是你實際學過的順序;不必跳章。
  4. 4. 之後若新增章節,會在「修訂紀錄」與目錄一起更新。

1. 學習地圖與檔案對照

1.1 你學過的內容(依時間)

階段內容對應檔案
入門第一個程式、外部套件hello.go
變數與常數var:=、區塊宣告、constA.go B.go C.go
輸出與型別fmt、基本型別、booluintD.go E.go F.go G.go
陣列宣告、索引、初始化、lenH.go i.go
Slicelen/cap、從陣列切片、makeJ.go
Slice 進階append、再切片、copyK.go
條件+=if / else / else if、巢狀 ifL.go
分支switch、多值 case、defaultM.go
迴圈三段式 forcontinue、巢狀、rangeN.go
函式參數、回傳值、多回傳、命名回傳O.go

1.2 怎麼跑單一檔案

因為很多檔案都是 package main + func main()不要在專案根目錄一次 go run .(會衝突)。

請這樣跑:

go run hello.go
go run A.go
go run J.go
# 依此類推
註:同一個資料夾裡若同時有多個 func maingo run . 會失敗。這是練習筆記型 repo 的常見狀況,之後若要做成正式專案,再拆成每個資料夾一個 main

2. 程式骨架:package / import / main

對應:hello.go

2.1 最小可執行結構

package main

import (
	"fmt"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Go())
}

每一段的意義:

部分作用
package main宣告這個檔案屬於 main 套件。要做成可執行程式,套件名必須是 main
import (...)引入要用的套件。標準庫如 fmt;外部套件如 rsc.io/quote(寫在 go.mod)。
func main()程式入口。程式從這裡開始跑。一個可執行程式只能有一個 main

2.2 必須記住的規則

  1. 1. 可執行程式 = C0 + C1

少一個就不是一般意義上的「可直接 go run 的小程式」。

  1. 2. 同一目錄多個 C0 會衝突

你早期的 H.goi.go 曾在同一個檔案裡貼多段 package main + func main(),那適合作筆記,但不適合作為單一可編譯單元。

  1. 3. 匯出規則(大小寫)

別的套件能用的名字,第一個字母要大寫。 例如 quote.Go()G 是大寫,所以外部套件才能呼叫。 若寫成小寫 quote.go,在套件外會無法使用。

  1. 4. C0 在做什麼

它記錄模組名稱與依賴版本。你專案裡有:

``text module example.com/m go 1.26.6 require rsc.io/quote v1.5.2 ``

沒有 go.mod 時,外部套件依賴會不好管理。

2.3 自測

  1. 1. 可執行 Go 程式的套件名必須是什麼?
  2. 2. quote.Go 為什麼能被外面用到?
  3. 3. 為什麼不建議在根目錄對這個 repo 直接 go run .
答案
  1. 1. main
  2. 2. 名稱以大寫開頭,屬於匯出符號
  3. 3. 多個檔案都有 func main(),會衝突

3. 變數宣告

對應:A.go

3.1 三種主要寫法

var A int     // 方式一:先宣告型別
A = 20        // 再賦值

var B = "apple" // 方式二:有初始值,可省略寫型別(編譯器推斷)

C := 0.2        // 方式三:短宣告(宣告 + 賦值一次完成)

3.2 對照表(務必背)

寫法可否在函式外可否型別推斷典型用途
var x int需寫型別,或之後給值要明確零值、或套件層級變數
var x = 1清楚、正式
x := 1❌ 只能在函式內日常最快寫法

3.3 零值(沒給初始值時)

Go 的變數宣告後一定有值,不會是「未定義垃圾值」。

型別零值
int 等數字0
float32 / float640
boolfalse
string""(空字串,不是 nil

3.4 :== 的差別

x := 10  // 第一次:宣告並賦值
x = 20   // 之後改值:用 =
// x := 20  // 在同一作用域再對同名用 :=,通常會出錯(除非至少有一個是新變數)

3.5 自測

  1. 1. := 能寫在 func 外面嗎?
  2. 2. var x int 不賦值,印出來是什麼?
  3. 3. C := 0.2 通常會推成什麼浮點型別?
答案
  1. 1. 不能
  2. 2. 0
  3. 3. 多半是 float64(字面量預設)

4. 區塊式 var 與多重宣告

對應:B.go

4.1 區塊寫法

var (
	A int
	B string = "as"
	C, d     = 1, 2
)
fmt.Println(A, B, C, d)
// A 是 0(零值),B 是 "as",C 是 1,d 是 2

意義:

  1. 1. var ( ... ) 只是一次寫多個變數,語意與分開寫多個 var 相同,但較整齊。
  2. 2. C, d = 1, 2:左邊幾個名字,右邊就要幾個值,數量必須對齊。
  3. 3. 沒給值的 A 會是該型別零值。

4.2 不同型別也可以一次短宣告

有人會問:「一邊是數字、一邊是字串,可以寫在同一行嗎?」

可以。重點不是「型別要一樣」,而是「左邊幾個名字,右邊幾個值」。

C, Z := 1, "hello"

✅ 這是合法的:

  • - 右邊有兩個值,左邊有兩個變數,數量對齊
  • - 編譯器會分別推斷型別:CintZstring
  • - 兩個變數不需要同一個型別

也可以寫成:

var C, Z = 1, "hello" // 同樣可以;型別一樣會被分別推斷

常見誤解:

想法實際
同一行的變數型別必須相同❌ 不必;各自依右邊的值推斷
:= 一次只能宣告一種型別❌ 可以混用多種型別
左邊 2 個、右邊 1 個也可以❌ 不行,數量一定要對齊

4.3 命名大小寫在這個階段的意義

package main 的練習檔裡,Aa 對「能不能跑」通常沒差。 但若之後拆成多個套件:

  • - 大寫開頭 = 可被其他套件使用(匯出)
  • - 小寫開頭 = 只在本套件內可用

現在先建立這個意識即可。

4.4 自測

  1. 1. var ( A int ) 裡的 A 初始值是?
  2. 2. C, d = 1, 2 若只寫一個右邊的值會怎樣?
  3. 3. C, Z := 1, "hello" 合法嗎?CZ 各是什麼型別?
答案
  1. 1. 0
  2. 2. 編譯錯誤(左右數量不一致)
  3. 3. 合法;CintZstring

5. 常數 const

對應:C.go(原版曾故意寫錯)

5.1 正確寫法

const (
	A int     = 1
	B         = 3.14
	C         = "Hi!"
	MaxRetry  = 26
)

5.2 硬規則

規則說明
編譯期就要確定const x = 1 + 2 ✅;依執行期才知道的值 ❌
同一區塊不能重名兩個 B 會編譯失敗
不能之後再改A = 2
可單行也可區塊const Pi = 3.14const ( ... )

5.3 你曾經踩過的錯

const (
	B = 3.14
	B = 26 // ❌ 同名重複
)

正確做法:第二個改名,例如 MaxRetry = 26。 「知道為什麼錯」本身就是學習的一部分。

5.4 自測

  1. 1. const 可以在執行時用 os.ReadFile 的結果當值嗎?
  2. 2. 常數定義後能用 = 改掉嗎?
答案
  1. 1. 不能(不是編譯期常數)
  2. 2. 不能

6. fmt 輸出與格式動詞

對應:D.go

6.1 三種輸出函式

var i, j string = "Hello", "World"

fmt.Println(i, j)        // 參數間自動空格,結尾自動換行 → Hello World
fmt.Print(i, j, "\n")    // 不自動空格、不自動換行;換行要自己加
fmt.Printf("i has value: %v and type: %T\n", i, i)
函式自動空格自動換行格式字串
Println
Print
Printf依你寫的格式通常自己加 \n

6.2 常用格式動詞

動詞意思i = "Hello"
%v預設值的樣子Hello
%T型別string
%#v接近 Go 語法的值"Hello"
%%印出一個 % 字元%

6.3 正確做法:同時印出字串和變數

如果你想要同時印出字串和變數,可以用:

fmt.Println("i has value:", i)

或是:

fmt.Printf("i has value: %v\n", i)

兩種差別:

寫法怎麼插變數換行
Println("...", i)用逗號把字串和變數分開傳進去自動換行
Printf("...%v\n", i)在字串裡留位置(%v),後面再補變數要自己寫 \n

記住:

  • - ❌ 不要把變數「黏」進字串中間卻沒正確收尾,例如 fmt.Print(i,j",\n")
  • - ✅ 要嘛逗號分開參數(Println / Print
  • - ✅ 要嘛用 Printf + 格式動詞(%v%T…)

6.4 你踩過的語法坑

fmt.Print(i,j",\n")   // ❌ 字串沒接好
fmt.Print(i, j, "\n") // ✅ 每個參數用逗號分開

6.5 自測

  1. 1. PrintlnPrint 差在哪兩件事?
  2. 2. 想看變數型別,用哪個動詞?
  3. 3. 想印出百分號本身,怎麼寫?
  4. 4. 想印出「i has value:」再加上變數 i,寫出兩種正確寫法。
答案
  1. 1. 自動空格、自動換行
  2. 2. %T
  3. 3. %%
  4. 4. fmt.Println("i has value:", i)fmt.Printf("i has value: %v\n", i)

7. 基本型別

對應:E.go

7.1 四個最常用的基礎型別

var a bool    = true   // 布林
var b int     = 5      // 整數
var c float32 = 3.14   // 32 位浮點
var d string  = "Hi!"  // 字串

7.2 宣告時「寫不寫型別」

var b int = 5 // 明確
var b = 5     // 推成 int
b := 5        // 函式內短宣告,推成 int

7.3 零值再強調一次

沒給初始值時:

  • - boolfalse
  • - 數字 → 0
  • - string""

這會直接影響你之後看 make([]int, 5) 為什麼一開始全是 0

7.4 自測

  1. 1. string 的零值是 nil 還是 ""
  2. 2. float32 與用 := 3.14 推出來的型別,一定相同嗎?
答案
  1. 1. ""
  2. 2. 不一定;:= 3.14 常推成 float64

8. bool 的四種宣告

對應:F.go

var b1 bool = true // 有型別 + 有值
var b2 = true      // 推斷型別 + 有值
var b3 bool        // 有型別、沒值 → 零值 false
b4 := true         // 短宣告(函式內)

只記一句: 沒給值的 bool,一定是 false,不是空、也不是 0

自測

  1. 1. var b bool 印出來是什麼?
  2. 2. 上面四種裡,哪一種只能寫在函式內?
答案
  1. 1. false
  2. 2. b4 := true

9. uint 與型別格式

對應:G.go

var x uint = 500
var y uint = 4500
fmt.Printf("Type: %T, value: %v\n", x, x)
fmt.Printf("Type: %T, value: %v\n", y, y)
型別意思
int可正可負
uint無符號,只能 ≥ 0

常用配對:

  • - %T → 印型別
  • - %v → 印值

自測

  1. 1. uint 能存 -1 嗎?
  2. 2. %T 印的是值還是型別?
答案
  1. 1. 不能(會編譯錯或超出語意)
  2. 2. 型別

10. 陣列 Array(基礎)

對應:H.goi.go 前半

10.1 宣告的兩種常見寫法

var arr1 = [...]int{1, 2, 3} // 長度由元素個數決定 → [3]int
arr2 := [5]int{4, 5, 6, 7, 8} // 固定長度 5

10.2 字串陣列、索引讀寫

cars := [4]string{"Volvo", "BMW", "Ford", "Mazda"}

prices := [3]int{10, 20, 30}
fmt.Println(prices[0]) // 10
fmt.Println(prices[2]) // 30

prices[2] = 50
fmt.Println(prices) // [10 20 50]

10.3 硬規則

  1. 1. 陣列長度是型別的一部分

[3]int[5]int 是不同型別,不能直接互塞。

  1. 2. index 從 0 開始

長度 3 的最後一個 index 是 2

  1. 3. 沒填的位置是零值

int0string""

10.4 自測

  1. 1. [3]int 最後一個 index 是?
  2. 2. prices[2] = 50 改的是第幾個元素(人類序數)?
  3. 3. [3]int[5]int 是不是同一型別?
答案
  1. 1. 2
  2. 2. 第 3 個
  3. 3. 不是

11. 陣列初始化細節與 len

對應:i.go 後半

11.1 三種初始化程度

arr1 := [5]int{}              // 全空 → 全零值
arr2 := [5]int{1, 2}          // 只填前面 → 後面補 0
arr3 := [5]int{1, 2, 3, 4, 5} // 填滿

輸出概念:

[0 0 0 0 0]
[1 2 0 0 0]
[1 2 3 4 5]

11.2 指定索引初始化

arr1 := [5]int{1: 10, 2: 40}
// index:  0  1   2  3  4
// 結果:  [0 10  40  0  0]

可以跳著填,例如:

[5]int{0: 7, 4: 99} // [7 0 0 0 99]

11.3 len()

arr1 := [4]string{"Volvo", "BMW", "Ford", "Mazda"}
arr2 := [...]int{1, 2, 3, 4, 5, 6}

fmt.Println(len(arr1)) // 4
fmt.Println(len(arr2)) // 6

重要:

  • - [4]string{...} → 長度你寫死
  • - [...]int{1,2,3} → 編譯器用元素個數推斷
  • - len 是「陣列長度」,不是「有幾個非零元素」

所以 [5]int{1, 2}len 仍是 5

11.4 陣列能力總表

能力寫法
固定長度[3]int{10, 20, 30}
長度自動[...]int{1, 2, 3}
讀元素a[0]
改元素a[2] = 50
部分初始化[5]int{1, 2}
指定位置[5]int{1: 10, 4: 9}
問長度len(a)

11.5 自測

  1. 1. [5]int{1, 2} 印出來是?
  2. 2. [5]int{1: 10, 2: 40}a[0] 是?
  3. 3. [5]int{1, 2}len 是 2 還是 5?
  4. 4. [...]string{"a", "b"} 長度是?
答案
  1. 1. [1 2 0 0 0]
  2. 2. 0
  3. 3. 5
  4. 4. 2

12. Slice 基礎(J.go)

這章開始是後來新增檔案的內容。
陣列是固定長度;slice 是 Go 日常更常用的動態序列視圖。

12.1 Array vs Slice

Array 陣列Slice 切片
宣告[3]int{1,2,3}[]int{1,2,3}(中括號裡沒有數字
長度固定,而且是型別的一部分可變
實務使用頻率較少很高
a := []int{2, 3} // 這是 slice,不是 array

12.2 lencap

a := []int{2, 3}
fmt.Println(len(a)) // 長度:現在有幾個元素
fmt.Println(cap(a)) // 容量:底層陣列目前最多能撐到哪
函數意思
len(s)你現在看得到、用得到的元素個數
cap(s)底層空間大小;在換新底層之前,len 最多可以長到這裡

比喻:

  • - len = 杯子裡現在有多少水
  • - cap = 杯子總共能裝多少

12.3 從陣列切出 slice:s[low:high]

var b = [5]int{1: 2} // [0, 2, 0, 0, 0]
c := b[1:4]          // 取 index 1,2,3(不含 4)

規則:含 low,不含 high

index:  0   1   2   3   4
值:     0   2   0   0   0
            ^-------^
            [1:4]

對這個例子:

  • - len(c) = 3
  • - cap(c) = 4(從切開點一路算到底層尾端)

重點:這樣切出來的 slice,通常與原陣列共用底層。 改 c[0],常常等於改到原陣列對應位置。

12.4 用 make 建立 slice

myslice1 := make([]int, 5, 10) // 型別, 長度, 容量
// [0 0 0 0 0], len=5, cap=10

myslice2 := make([]int, 5) // 省略容量時,cap = len
// [0 0 0 0 0], len=5, cap=5
寫法lencap
make([]int, 5, 10)510
make([]int, 5)55
[]int{2, 3}2(通常)2

元素一開始是零值;對 int 就是 0

12.5 三種建立 slice 的方式(你都練過)

  1. 1. 字面量:[]int{2, 3}
  2. 2. 從陣列切:arr[low:high]
  3. 3. make([]T, len, cap)

12.6 自測

  1. 1. [3]int[]int 哪個是 slice?
  2. 2. s[1:4] 含不含 index 4
  3. 3. make([]int, 5, 10) 的 len / cap?
  4. 4. 從陣列切出的 slice,改元素會不會影響原陣列?
答案
  1. 1. []int
  2. 2. 不含
  3. 3. 5 / 10
  4. 4. 通常會(共用底層)

13. append / 再切片 / copy(K.go)

13.1 append:往後面加元素

myslice1 := []int{1, 2, 3, 4, 5, 6}
myslice1 = append(myslice1, 20, 21)
// [1 2 3 4 5 6 20 21]

硬規則:

s = append(s, 值...)

append 的結果一定要接住(賦值回去,或接到新變數)。 只呼叫 append(s, 20) 卻不賦值,等於白做。

13.2 合併兩個 slice:要用 ...

myslice2 := []int{7, 8}
myslice3 := append(myslice1, myslice2...)
寫法意思
append(a, 20, 21)加個別元素
append(a, b...)把另一個 slice「拆開」接上去
append(a, b)通常 ❌(型別不對)

:==

  • - myslice3 := append(...) → 新變數
  • - myslice1 = append(...) → 覆蓋原變數

13.3 從陣列切片,再切片改長度

arr1 := [6]int{9, 10, 11, 12, 13, 14}
myslice1 := arr1[1:5] // [10 11 12 13]
// len=4;cap 則從切開點算到底層尾端

myslice1 = arr1[1:3]  // [10 11],把「窗戶」切短
myslice1 = append(myslice1, 20, 21, 22, 23) // 再變長

心智模型:

  • - 切片像一扇窗戶,看著底層陣列某一段
  • - len = 窗戶現在開多大
  • - cap = 不換新底層時,窗戶最多還能往右開到哪
  • - 若 append 後超過 cap,Go 會配置新的底層陣列

13.4 copy:獨立副本

numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
neededNumbers := numbers[:len(numbers)-10] // 前 5 個
numbersCopy := make([]int, len(neededNumbers))
copy(numbersCopy, neededNumbers)
方式改副本會不會動到原本?
s2 := s1[1:3](切片)常常會(共用底層)
copy(dst, src)不會(值被拷貝走了)

copy 要注意:

  1. 1. 目標 dst 要先有足夠長度(常用 make
  2. 2. 實際拷貝個數 = min(len(dst), len(src))
  3. 3. copy 不負責把 slice「加長」;要加長仍用 append

13.5 一張圖串起來

建立 []int{...} / make / arr[a:b]
        │
        ▼
   看 len / cap
        │
        ├─ append(s, x)      → 加長(可能換底層)
        ├─ append(a, b...)   → 合併
        ├─ s[a:b]            → 再切片(常共用底層)
        └─ copy(dst, src)    → 獨立複製

13.6 自測

  1. 1. append 之後一定要怎樣,結果才留得住?
  2. 2. 合併 slice 為什麼要寫 b...
  3. 3. s2 := s1[1:3]copy 最大差別?
  4. 4. 長度 15 的 slice 做 s[:len(s)-10],會得到幾個元素?
答案
  1. 1. 賦值回去(或賦給新變數)
  2. 2. 把元素拆開才能接上
  3. 3. 是否共用底層
  4. 4. 5 個

14. 運算與 if / else(L.go)

14.1 先會改值:+=

var x = 10
x = x + 5 // 等同 x += 5
fmt.Println(x) // 15

常見還有:-= *= /=

14.2 單純 if

x := 20
y := 18
if x > y {
	fmt.Println("x is greater than y")
}

規則:

  1. 1. 條件不必加括號:if x > y 即可
  2. 2. { 一定要有,即使只有一行
  3. 3. 條件為 true 才進入大括號

常見比較運算:> < >= <= == !=

14.3 if / else(最容易踩坑)

temperature := 14
if temperature > 15 {
	fmt.Println("It is warm out there")
} else {
	fmt.Println("It is cold out there")
}

Go 硬規則:C0 必須緊接在上一行 C1 後面,不能單獨換行。

// ✅ 正確
if ok {
	...
} else {
	...
}

// ❌ 編譯失敗
if ok {
	...
}
else {
	...
}

原因:Go 會在換行的 } 後自動插入分號,導致 else 語法被拆壞。

14.4 else if 多分支

time := 22
if time < 10 {
	fmt.Println("Good morning.")
} else if time < 20 {
	fmt.Println("Good day.")
} else {
	fmt.Println("Good evening.")
}

執行順序:

  1. 1. 先看第一個 if
  2. 2. 不行再看 else if
  3. 3. 都不行才走最後 else
  4. 4. 最多只會進其中一個分支

14.5 巢狀 if

num := 20
if num >= 10 {
	fmt.Println("Num is more than 10.")
	if num > 15 {
		fmt.Println("Num is also more than 15.")
	}
} else {
	fmt.Println("Num is less than 10.")
}

若外層條件為 false,內層整段都不會執行。

14.6 自測

  1. 1. else 能不能寫成下一行單獨一個 else
  2. 2. else if 鏈最多會執行幾個分支?
  3. 3. 外層 if 為 false 時,內層會跑嗎?
  4. 4. x = x + 5 的短寫?
答案
  1. 1. 不能
  2. 2. 最多一個
  3. 3. 不會
  4. 4. x += 5

15. switch(M.go)

15.1 基本寫法

day := 8

switch day {
case 1:
	fmt.Println("Monday")
case 2:
	fmt.Println("Tuesday")
case 3:
	fmt.Println("Wednesday")
case 4:
	fmt.Println("Thursday")
case 5:
	fmt.Println("Friday")
case 6:
	fmt.Println("Saturday")
case 7:
	fmt.Println("Sunday")
default:
	fmt.Println("Not a weekday")
}

day = 8 時沒有對應 case,會走 default

15.2 和 if 的關係

當你在對「同一個變數等於很多固定值」做分支時,switch 通常比一長串 else if 更清楚。

15.3 Go 的 switch 三個關鍵

  1. 1. 自動 break

進了某個 case,跑完就結束,不會掉進下一個 case。 這點和 C 不同。

  1. 2. C0 可有可無

沒有任何 case 命中時才會進 default

  1. 3. C0 後面直接寫程式即可

不必再為每個 case 額外包一層 {}(整個 switch 已有大括號)。

15.4 一個 case、多個值

day := 5

switch day {
case 1, 3, 5:
	fmt.Println("Odd weekday")
case 2, 4:
	fmt.Println("Even weekday")
case 6, 7:
	fmt.Println("Weekend")
default:
	fmt.Println("Invalid day of day number")
}

等同於:

if day == 1 || day == 3 || day == 5 {
	...
}

15.5 何時用 switch、何時用 if

情況較適合
同一變數比很多固定值switch
複雜條件、範圍比較、多個變數if / else if
星期、狀態碼、模式選擇常見用 switch

15.6 自測

  1. 1. Go 的 case 跑完會不會自動掉進下一個 case?
  2. 2. 什麼時候會進 default
  3. 3. case 1, 3, 5: 表示什麼?
  4. 4. 在多值 case 的例子裡,day := 6 會印什麼?
答案
  1. 1. 不會
  2. 2. 沒有 case 命中時
  3. 3. 值為 1 或 3 或 5 都算這一支
  4. 4. Weekend

16. for / continue / range(N.go)

16.1 經典三段式 for

for i := 0; i <= 100; i += 10 {
	fmt.Println(i)
}
// 0 10 20 ... 100
角色例子
1初始i := 0
2條件(true 才繼續)i <= 100
3每次迴圈結束後執行i += 10

Go 只有 C0,沒有 while 關鍵字。 若要 while 風格:

for i < 5 {
	...
}

若要無限迴圈:

for {
	...
	// 通常內部會配 break
}

16.2 continue vs break

for i := 0; i < 5; i++ {
	if i == 3 {
		continue
	}
	fmt.Println(i)
}
// 0 1 2 4(沒有 3)
關鍵字效果
continue結束這一圈剩餘程式,進入下一圈
break結束整個迴圈

若把上面的 continue 改成 break,則只會印 0 1 2

16.3 巢狀 for

adj := [2]string{"big", "tasty"}
fruits := [3]string{"apple", "orange", "banana"}

for i := 0; i < len(adj); i++ {
	for j := 0; j < len(fruits); j++ {
		fmt.Println(adj[i], fruits[j])
	}
}

外圈每 1 次,內圈完整跑 3 次,總共 2 × 3 = 6 行:

big apple
big orange
big banana
tasty apple
tasty orange
tasty banana

len(...) 當上限,陣列變長時比較不容易寫死錯誤數字。

16.4 range 遍歷

fruits := [3]string{"apple", "orange", "banana"}

for idx, val := range fruits {
	fmt.Printf("%v\t%v\n", idx, val)
}
寫法意思
for i, v := range s要索引 + 值
for _, v := range s只要值
for i := range s只要索引

range 可用於 array、slice、string、map 等。 之後處理 slice 時,這會是最常見寫法之一。

16.5 四種 for 心智圖

for i := 0; i < n; i++   → 計數型
for 條件 { }             → while 型
for { }                  → 無限迴圈(常配 break)
for i, v := range 容器    → 遍歷型(最常見)

16.6 自測

  1. 1. Go 有沒有 while 關鍵字?
  2. 2. continuebreak 差在哪?
  3. 3. 外圈 2、內圈 3,共印幾行?
  4. 4. 只要值不要 index,range 怎麼寫?
答案
  1. 1. 沒有(用 for 模擬)
  2. 2. 跳過本圈 vs 結束整個迴圈
  3. 3. 6
  4. 4. for _, v := range s

17. 函式 func(O.go)|完整 30 分鐘課

對應:O.go

教學格式(固定): 每個程式碼框下方都有兩段說明
1) 這段加了什麼 2) 為什麼要這樣寫

17.0 這 30 分鐘怎麼上

時間做什麼對應小節
0–5 分go run O.go,先看輸出17.1
5–12 分無參數/有參數/有回傳17.2~17.5
12–18 分多回傳、_、命名回傳17.6~17.7
18–25 分slice 包函式 + 作業 avg17.8~17.9
25–30 分自測 + 對答案17.10~17.11
go run O.go
這段加了什麼
  • 單獨執行 O.go 這個練習檔
為什麼要這樣寫

這個 repo 很多檔都有自己的 main,用 go run . 會衝突,所以改跑單一檔。


17.1 為什麼需要函式?

沒有函式時,同樣邏輯會一直複製貼上。有了函式,你可以:

  1. 1. 給一段邏輯取名字addsumSlice
  2. 2. 重複呼叫,不必重寫
  3. 3. 把大問題拆小(之後本地 AI:載入模型/推理/回傳結果,各自都會是函式)

17.2 最基本的形狀

func 函式名(參數名 型別) 回傳型別 {
	// 函式本體
	return 值
}
這段加了什麼
  • func:宣告「這是一個函式」
  • 函式名:之後呼叫用的名字
  • (參數名 型別):外面要傳進來的資料
  • 回傳型別:算完後送出的結果型別
  • return 值:真正把結果送回去
為什麼要這樣寫

Go 要求輸入、輸出都寫清楚,編譯器才能檢查型別,你之後重讀也才知道這函式負責什麼。

型態例子
無參數、無回傳func sayHello() { ... }
有參數、無回傳func greet(name string) { ... }
有參數、有回傳func add(a, b int) int { ... }
多回傳func divmod(a, b int) (int, int) { ... }

17.3 無參數、無回傳(約第 5~7 分)

func sayHello() {
	fmt.Println("Hello, Go function!")
}
這段加了什麼
  • 定義 sayHello
  • () 空的 → 不用外面傳資料
  • 後面沒有回傳型別 → 只做事,不回傳值
為什麼要這樣寫

有些動作「做完就好」(例如印歡迎訊息),不需要算出一個值給外面。

func main() {
	sayHello()
}
這段加了什麼
  • main 裡呼叫 sayHello()
為什麼要這樣寫

定義不會自動執行;一定要呼叫。 sayHello 是函式本身;sayHello() 才是「請執行」。


17.4 有參數(約第 7~10 分)

func greet(name string) {
	fmt.Println("你好,", name)
}
這段加了什麼
  • 參數 name string:呼叫時要傳字串進來
  • 函式內用 name 印出不同名字
為什麼要這樣寫

若每次打招呼都複製整段 Println 只改名字,程式會很肥。把會變的部分做成參數,一個函式就能服務很多人名。

greet("惟理")
這段加了什麼
  • 呼叫 greet,傳入 "惟理"name
為什麼要這樣寫

傳入值的型別必須符合定義。這裡是 string,所以傳字串。


17.5 有回傳值(約第 10~12 分)

func add(a int, b int) int {
	return a + b
}

sum := add(3, 5)
fmt.Println(sum) // 8
這段加了什麼
  • 兩個 int 參數,回傳一個 int
  • return a + b 把結果送出
  • sum := add(3, 5) 在呼叫端接住結果
為什麼要這樣寫

return 負責送出;sum := 負責接住。兩邊要配合,算出來的值才不會丟掉。 宣告了回傳型別卻不 return,會編譯失敗。

func sub(a, b int) int {
	return a - b
}
這段加了什麼
  • a, b int:同型別參數縮寫
  • 作業函式 sub:回傳 a - b
為什麼要這樣寫

型別相同可少寫一次 int。減法跟加法一樣值得包成可重用函式(O.go 已內建,可對照)。


17.6 多回傳值(約第 12~16 分)

func divmod(a, b int) (int, int) {
	return a / b, a % b
}
這段加了什麼
  • (int, int):一次回傳兩個整數
  • return a / b, a % b:商、餘數
為什麼要這樣寫

成對結果(商與餘、值與是否存在)用多回傳最自然,不必硬塞怪結構。

q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
這段加了什麼
  • 左邊兩個變數分別接兩個回傳值
為什麼要這樣寫

回傳幾個,左邊就要幾個位子。數量不對會編譯失敗。

q, _ := divmod(17, 5)
這段加了什麼
  • _:丟掉不需要的回傳值
為什麼要這樣寫

Go 不允許宣告了變數卻不用。只想要商時,餘數用 _ 丟掉,既合法也表達「故意不要」。

value, ok := someMap[key]
result, err := doSomething()
這段加了什麼
  • 預告兩種常見多回傳:okerr
為什麼要先看一眼

現在不用會寫,但要先知道 Go 很愛用多回傳表達額外狀態。之後學 maperror 會正式用到。


17.7 命名回傳(約第 16~18 分)

func rectArea(width, height int) (area int) {
	area = width * height
	return
}
這段加了什麼
  • (area int):回傳值先取名
  • area 賦值後可用裸 return
為什麼要這樣寫

回傳值若在函式內被改多次,取名會較清楚。 初學多數情況仍建議明確 return 值;命名回傳先會看即可。


17.8 把 slice + for 包進函式(約第 18~22 分)

func sumSlice(nums []int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}
這段加了什麼
  • 參數 nums []int
  • 用已學的 for range 累加
  • return total
為什麼要這樣寫

加總你在 N.go 已會。包成函式後,任何地方都能 sumSlice(某slice),不必每次重寫迴圈。

func maxSlice(nums []int) int {
	if len(nums) == 0 {
		return 0
	}
	m := nums[0]
	for _, n := range nums[1:] {
		if n > m {
			m = n
		}
	}
	return m
}
這段加了什麼
  • 先處理空 slice
  • 以第一個元素當初始最大值,再往後比較
為什麼要這樣寫

空 slice 不能取 [0]。 「先假設第一個最大,再比較」是找最大值的安全寫法。


17.9 作業完成版:平均值 avg(約第 22~25 分)

上一版作業有兩題:subavgsub 已在 17.5;這裡補完整 avg

func avg(nums []int) float64 {
	if len(nums) == 0 {
		return 0
	}
	return float64(sumSlice(nums)) / float64(len(nums))
}
這段加了什麼
  • 回傳型別改成 float64(可以有小數)
  • 空 slice 先回傳 0,避免除以 0
  • float64(sumSlice(nums)) / float64(len(nums)):先轉浮點再除
為什麼要這樣寫

若用 int 相除,10/4 會變成 2(小數被砍掉)。平均值需要小數,所以要轉成 float64。 重用 sumSlice 也示範「函式可以呼叫函式」。

nums := []int{10, 20, 30, 40}
fmt.Println(avg(nums)) // 25
這段加了什麼
  • 建立測試資料並呼叫 avg
為什麼要這樣寫

寫完函式一定要呼叫驗證;不要只定義不測。


17.10 易錯點(約第 25~27 分)

  1. 1. 忘了呼叫括號

sayHellosayHello()

  1. 2. 回傳型別與 C0 不一致

宣告 intreturn "hi" → 編譯錯誤

  1. 3. 多回傳沒接完

回傳兩個值,左邊只寫一個變數 → 編譯錯誤

  1. 4. 整數除法當平均

sum/len 若都是 int 會丟小數;平均要用 float64

  1. 5. 參數是複本(基本型別)

函式內改 a,通常不影響外面(之後指標章再細講)


17.11 自測(約第 27~30 分)

  1. 1. func add(a, b int) int 裡,a, b int 是什麼意思?
  2. 2. 多回傳時,左邊變數數量要怎樣?
  3. 3. 不需要的回傳值用什麼丟掉?
  4. 4. 為什麼 avg 要用 float64,不能直接 int 相除?
  5. 5. 空 slice 呼叫 maxSlice 為什麼要先檢查 len
答案
  1. 1. 兩個參數都是 int 的縮寫
  2. 2. 要與回傳個數對齊(或用 _ 丟)
  3. 3. _
  4. 4. int 相除會捨去小數,平均值需要小數
  5. 5. 空的不能取 nums[0],會當掉

18. 總複習對照表

18.1 語法工具箱

主題你要能立刻想起
程式入口package main + func main()
變數var / := / 零值
常數const、不能重名、不能改
輸出Print / Println / Printf + %v %T %#v
陣列長度固定、[n]T、index 從 0
Slice[]Tlen/capmake、切片語法
成長與複製appendb...copy
條件if / else if / else(else 不可換行)
多值分支switch / case a, b / default
迴圈三段式 forcontinuebreakrange
函式參數、return、多回傳、命名回傳

18.2 最容易混淆的幾組概念

  1. 1. C0 vs C1

前者陣列,後者 slice。

  1. 2. C0 vs C1

現在有幾個 vs 底層還能撐多大。

  1. 3. 切片 vs C0

切片常共用底層;copy 是獨立副本。

  1. 4. C0 vs C1

跳過本圈 vs 離開迴圈。

  1. 5. C0 vs C1

複雜條件用 if;多個固定值比對用 switch

  1. 6. C0 定義 vs 呼叫

定義寫 func name(...);呼叫寫 name(...)

18.3 建議的下一步(尚未寫進正式教學正文)

這些是「接下來可以學」,但還不是本手冊已教完的內容

  1. 1. map
  2. 2. struct
  3. 3. 指標(pointer)與函式參數傳遞
  4. 4. 錯誤處理 error(常搭配多回傳)

等我們正式教完,會把對應章節補進來。


19. 修訂紀錄

日期變更
2026-09-06初版建立。涵蓋從第一次複習教學到 J.goN.go 的全部已教內容。
2026-09-09補上兩處遺漏:① 不同型別一次短宣告 C, Z := 1, "hello";② 同時印出字串與變數的正確 Println / Printf 寫法。並發布 GitHub Pages 視覺報告。
2026-09-11新增第 17 章函式 funcO.go):參數、回傳、多回傳、命名回傳,以及 slice 總和/最大值小練習。
2026-09-13調整教學格式:每個程式碼區塊下方都附「這段加了什麼/為什麼要這樣寫」;先套用到第 17 章函式。
2026-09-13完善函式 30 分鐘課:補齊時間表、作業 sub/avg 完整說明與參考答案,並更新 O.go

更新約定(給未來的我們)

對,之後每次學習結束,都會更新這份手冊:

  1. 1. 新學的語法 → 新增一章(或在既有章節補小節)
  2. 2. 你踩過的坑 → 寫進該章的「易錯點」
  3. 3. 對應練習檔 → 更新「學習地圖與檔案對照」
  4. 4. 修訂紀錄 → 追加一列日期與變更說明

這樣手冊會一直等於「到目前為止,我們真正教過、你真正練過」的完整版本,而不是一份預先寫好、超前很多的大綱。

配對檔案:學習手冊.md學習報告.htmlindex.html
修訂:2026-09-13 · 完善函式 30 分鐘課