CodeGym /課程 /Go SELF /將套件視為 namespace — 命名規則

將套件視為 namespace — 命名規則

Go SELF
等級 10, 課堂 1
開放

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
fmt.Println, strconv.Atoi
是 第一個字母是大寫
Unexported
fmt.someHelper, internalCalc
否 第一個字母是小寫

是的,這看起來簡單得有點不像真的。但它真的就是這樣運作。這也是很多人喜歡 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 的一部分,也就會被人閱讀。而人類是很脆弱的:如果不方便,他們就會換個套件。

「更易讀」與「較不易讀」的迷你表

目的 較好 較差 原因
公開函式
PrintReport
PRINT_REPORT
Go 不習慣全大寫在那邊「大喊」
內部助手
formatReport
Format_report
_ 很少用在名稱裡
區域變數
income
i
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 很正常,但重要的是不要把它變成「垃圾場」。

1
任務
Go SELF, 等級 10, 課堂 1
上鎖
倉儲加總工具
倉儲加總工具
1
任務
Go SELF, 等級 10, 課堂 1
上鎖
公開名稱
公開名稱
1
任務
Go SELF, 等級 10, 課堂 1
上鎖
避免遮蔽的報表
避免遮蔽的報表
1
任務
Go SELF, 等級 10, 課堂 1
上鎖
內部報表
內部報表
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION