1. 套件與命名空間
當你寫第一批程式時,單一檔案 main.go 看起來很正常。但過了幾天,出現十幾個函式、幾個常數、一些變數,然後檔案突然開始像一個衣櫃:你「先隨手」把東西放進去,接著又「先隨手」塞進一些別的東西,最後你已經住進衣櫃裡了。
Go 的套件正好解決這個問題:它讓你能夠 依語意分組程式碼,並提供規則告訴你誰能「看見」什麼、誰能使用什麼。結果你的專案會像整齊的廚房抽屜,而不是一鍋裡面同時有湯匙、襪子,還有一個「為什麼它會運作」的謎題。
套件作為 namespace:「點號前的名稱」不是魔法
如果你已經寫過 fmt.Println("hello"),那你就看過套件存在的最重要症狀:點號。
fmt.Println 讀作:「在 fmt 套件中,取出已匯出的名稱 Println。」也就是說,套件其實就是 命名空間,它保護你避免程式裡同時存在 15 個叫做 Print() 的函式,卻不知道哪一個才是「那個」。
可以把它想成這樣:
- 套件是姓氏,
- 函式/型別/變數是名字,
- 而 fmt.Println 就是「Petrov Ivan」,只不過是給程式用的。
而且好消息是:在 Go 裡這不是約定俗成,而是語言的真實機制。
package 這一行:檔案如何屬於套件
任何 .go 檔案(除了今天先不談的特殊情況)都會以 package 行開始——也就是 package <name>。
要抓住一個重點:檔案本身不是「程式的單位」。單位是 套件,而檔案只是套件中的一小塊。
先看一個最小範例:
package main
import "fmt"
func main() {
fmt.Println("hi") // hi
}
package main 這行表示:「這個檔案屬於 main 套件。」
接著是個很實用、能省下很多時間的事實:同一資料夾內所有具有相同 package 的檔案,都會被視為同一個套件。也就是說,它們會共享套件層級的可見範圍。
Package-level scope:套件內的可見性
這裡有個很重要的點:在同一個套件內,所有檔案都能看見彼此的宣告。不是「如果有 import」,不是「如果有連結」,而是直接就能看見,因為它們本來就是同一個套件。
我們繼續前面的教學小程式。假設這是一個最簡單的「每日收支」:讀入收入與支出,計算差額並印出報表。以前你可能會把所有東西都寫在 main.go。今天我們開始把程式碼拆到不同檔案,但暫時仍維持同一個套件:main。
檔案 main.go
在這個檔案裡,我們會有「流程」:讀取、計算、輸出。
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
balance := income - expense
fmt.Println(formatReport(income, expense, balance))
}
注意:這裡的 readIncomeExpense 和 formatReport 尚未宣告。這完全沒問題——它們可以存在於同一個套件的其他檔案中。
檔案 io.go
現在把輸入讀取拆成獨立函式,放在另一個檔案裡,但仍然是 同一個套件:
package main
import "fmt"
func readIncomeExpense() (int, int) {
var income int
var expense int
fmt.Scan(&income, &expense)
return income, expense
}
檔案 report.go
接著也把報表格式化獨立出來:
package main
import "fmt"
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
重點在於,這三個檔案都是 同一個套件 main 的一部分,所以它們可以自由呼叫彼此的函式。這就是 package-level scope:套件層級的可見範圍。
如果你想把這件事裝進腦中,可以看這個圖:
flowchart TD
A[專案資料夾] --> B[main.go package main]
A --> C[io.go package main]
A --> D[report.go package main]
B --> E[同一個套件:main]
C --> E
D --> E
E --> F[套件層級的共用名稱:readIncomeExpense, formatReport, ...]
如果你開始想:「所以檔案不是邊界嗎?」答案是:邊界是套件,不是檔案。這非常 Go:更少的實體,就更少的混亂。
2. package main 與進入點
一般來說,套件只是程式碼容器。但 main 是特例:程式是從 main 套件開始執行的,而進入點是 main() 函式。
可以這樣說:如果套件像「零件盒」,那麼 package main 就是那個會組成 可執行程式 的盒子,而不是函式庫。
還有一個有用的事實:當 Go 分析程式時,它會查看每個 main 套件的進入點——其中包含 main 函式,也包含套件初始化機制。
初始化我們之後再單獨講;今天先掌握這個概念就夠了:main 是「應用程式入口」,通常用來放「把流程串起來」的部分。
3. 匯出:公開與內部 API
可匯出與不可匯出的名稱:依第一個字母決定
現在來到這堂課的核心主題,也就是我們今天要解的重點。
在 Go 裡有一條非常簡單的規則:
- 名稱以大寫字母開頭 → 它是 已匯出(exported)的,其他套件可以使用;
- 名稱以小寫字母開頭 → 它是 未匯出(unexported)的,只能在自己的套件內使用。
這個規則適用於函式、變數、常數、型別(之後還會包含方法與結構欄位)。
標準函式庫範例:fmt.Println vs 「那如果是 fmt.println 呢?」
我們直接來「摸摸看」這條規則:
package main
import "fmt"
func main() {
fmt.Println("ok") // ok
// fmt.println("no") // compilation error: no such exported name
}
為什麼大寫的 Println 存在,而小寫的 println 不存在?因為 fmt 套件 匯出 了 Println,也就是把它作為公開 API 的一部分。
表格,讓大腦不要假裝「我已經懂了」
| 名稱 | 範例 | 可從其他套件存取嗎? | 原因 |
|---|---|---|---|
| Exported | |
是 | 第一個字母是大寫 |
| Unexported | |
否 | 第一個字母是小寫 |
是的,這看起來簡單得有點不像真的。但它真的就是這樣運作。這也是很多人喜歡 Go 的原因之一:規則越少,意外驚喜就越少。
匯出:在套件內與套件外
這時很容易混淆:「如果我的 formatReport 是小寫,那 main.go 就不能呼叫它嗎?」可以,因為 main.go 和 report.go 在同一個套件裡。
匯出/未匯出 只影響套件之間的存取。在同一套件內,兩者都可使用。
我們來改進一下程式,讓 formatReport 成為內部細節,而對外(未來)則暴露一個更「人類可讀」的函式。雖然現在仍在同一個套件裡,存取上不會有差別,但語意會更清楚。
檔案 report.go(帶有內部函式的版本)
package main
import "fmt"
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
這是一個未匯出的函式——「內部助手」。
檔案 main.go(使用內部函式)
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
balance := income - expense
fmt.Println(formatReport(income, expense, balance))
}
可以運作,因為套件相同。
但如果我們嘗試從 另一個 套件呼叫 formatReport,它就無法使用。這正是小寫字母的意義:「別碰,這是內部廚房」。
匯出是一種合約,而不是「秘密性」
人很容易以為「未匯出」和安全性有關。其實不是;它首先是關於 合約與承諾。
當你匯出一個名稱時,你就像是在對外界說:「這個可以用。我會盡量不要無緣無故把它弄壞。」因此,套件的公開 API 就是一組已匯出的識別符:常數、型別、變數、函式,以及(等我們講到結構時)已匯出的欄位與方法。
由此也導出一個對初學者很實際的設計原則:不要什麼都匯出。如果你把每個小助手函式都做成公開的,你就會把套件變成週六的菜市場:看起來什麼都有,但你找不到需要的東西,而攤販(也就是一週後的你)甚至不記得東西放在哪。
為了感受這個概念,想像你正在做一個「餘額計算」套件(未來我們會學怎麼把這種東西拆成獨立套件)。公開合約很適合放像 ComputeBalance(...) 這種東西;而 parseIncomeFromWeirdLegacyFormat(...) 最好留在內部:它有用,但它不應該變成外部程式碼的「承諾」。
4. 命名規則與小型重構
命名規則:所有東西該怎麼取名
Go 裡的名稱不只是「美觀」。因為有套件與匯出機制,名稱會變成架構的一部分。若名稱取得不好,受傷的不只有眼睛,還有 import、閱讀程式碼,以及其他所有地方。
先從新手最常踩的坑開始:用套件名稱當變數名稱。這尤其令人沮喪,因為錯誤看起來像是「昨天還能跑,今天卻不行」,而原因其實只有一行。
範例:如何不小心「弄壞 fmt」
package main
import "fmt"
func main() {
fmt.Println("before") // before
fmt := "oops"
_ = fmt
// fmt.Println("after") // error: fmt is now a variable, not a package
}
在 main 裡,你建立了一個叫做 fmt 的變數,並且在這個可見範圍裡把 fmt 套件「遮住」了。沒錯,編譯器會誠實地救你,讓你無法編譯,但它不會幫你修心情。
第二個重要原則:已匯出的名稱應該 簡單且可預測。一旦你把名稱做成公開,它就成了 API 的一部分,也就會被人閱讀。而人類是很脆弱的:如果不方便,他們就會換個套件。
「更易讀」與「較不易讀」的迷你表
| 目的 | 較好 | 較差 | 原因 |
|---|---|---|---|
| 公開函式 | |
|
Go 不習慣全大寫在那邊「大喊」 |
| 內部助手 | |
|
_ 很少用在名稱裡 |
| 區域變數 | |
|
i 在迴圈中可以,但不適合表達語意 |
Go 非常重視可讀性,而套件又會強化這件事:名稱不只在檔案裡可見,還會在其他地方透過「點號」出現。
微實作:隱藏細節並建立 API
為了把「匯出 = 合約」這個觀念釘牢,我們來對這個小程式做一個迷你重構。我們會加入一個負責報表的「主函式」,並把內部細節藏進小寫名稱裡。
檔案 report.go(公開 + 內部)
package main
import "fmt"
func PrintBalanceReport(income, expense int) string {
balance := income - expense
return formatReport(income, expense, balance)
}
func formatReport(income, expense, balance int) string {
return fmt.Sprintf("income=%d expense=%d balance=%d", income, expense, balance)
}
我們現在仍然在 main 套件裡,所以這比較像是一個「思路」上的練習。但它很重要:你會開始即使在同一套件內也建立 API,這樣未來要把程式碼拆到其他套件時會更容易。
檔案 main.go(使用公開函式)
package main
import "fmt"
func main() {
income, expense := readIncomeExpense()
fmt.Println(PrintBalanceReport(income, expense)) // income=... expense=... balance=...
}
從程式碼的角度來看,閱讀起來更順了:main 現在對報表細節知道得更少。
5. 典型錯誤
錯誤 1:以為「匯出」會影響同一套件內不同檔案之間的存取。
這是初學者在接觸過「檔案是邊界」的語言後很常見的混淆。在 Go 裡,邊界是套件。如果兩個檔案都宣告為 package main,而且放在同一資料夾,它們就在同一個套件裡,並且可以在 package-level 宣告層級看見彼此。匯出管理的是套件與套件之間的存取,而不是檔案與檔案之間。
錯誤 2:試圖呼叫 fmt.println,或自行「猜」套件中的名稱。
新手有時會這樣想:「既然 Println 會印出來,那 println 也應該會印吧。」不會。Go 對其他套件中的名稱存取,是由匯出決定的:如果名稱未匯出,你就看不到它。正確做法是打開套件文件或 IDE 提示,而不是猜「它到底叫什麼」。
錯誤 3:為了以防萬一,把所有東西都匯出。
當你把字母改成大寫時,你其實是在公開承諾支援這個名稱。在大型專案裡,公開 API 是很嚴肅的責任:它更難改、也更需要維護。因此最好從一開始就習慣只匯出真正像入口點的東西,其餘輔助內容都留在套件內。Go 的基本觀念之一,就是公開 API 是由套件中已匯出的識別符組成的。
錯誤 4:用同名變數把套件「弄壞」了,例如 fmt := ...)。
這看起來像小事,卻會破壞閱讀並引發非常惱人的編譯錯誤。如果你已經 import 了某個套件,就不要再拿它的名字當變數名。若你很想寫 fmt := ...,請先停一下,改用符合語意的名稱:msg、line、text、report。
錯誤 5:把 package main 的意義和一般套件混淆。
有時候會想:「乾脆到處都寫 package main,這樣就能跑。」當然,它一段時間內也許真的能跑……但 main 是應用程式套件、是入口點、是用來把事情「串起來」的地方。在真實程式裡,你幾乎總是希望大部分邏輯不在 main,而是在其他套件裡;main 只負責把一切組裝起來。雖然我們現在用的是小範例,main 很正常,但重要的是不要把它變成「垃圾場」。
GO TO FULL VERSION