了解Go中的包可見性

簡介

在創建一個Go語言的套件時,最終目標通常是使該套件可供其他開發者使用,無論是在更高級的套件中還是整個程式中。通過導入該套件,您的代碼可以作為其他更複雜工具的構建塊。然而,只有某些套件可供導入。這取決於套件的可見性。

可見性在這種情況下指的是可以引用套件或其他構造的文件空間。例如,如果我們在一個函數中定義一個變數,該變數的可見性(範圍)僅限於定義它的函數內部。同樣地,如果您在一個套件中定義一個變數,您可以使其僅對該套件可見,或者允許它對套件外部也可見。

在編寫符合人體工學的代碼時,仔細控制包的可見性非常重要,尤其是在考慮到未來可能對包進行的更改時。如果您需要修復錯誤、提高性能或更改功能,您會希望以一種不會破壞使用您包的任何人代碼的方式進行更改。減少破壞性更改的一種方法是僅允許訪問包中正確使用所需的那些部分。通過限制訪問,您可以在內部對包進行更改,從而減少影響其他開發人員使用包的機會。

在本文中,您將學習如何控制包的可見性,以及如何保護僅應在包內部使用的代碼部分。為此,我們將創建一個基本記錄器來記錄和調試消息,使用具有不同程度項目可見性的包。

先決條件

要跟隨本文中的示例,您需要:

.
├── bin 
│ 
└── src
    └── github.com
        └── gopherguides

導出與未導出項目

與Java和Python等使用訪問修飾符publicprivateprotected來指定範圍的其他編程語言不同,Go通過聲明方式來確定項目是導出還是未導出。在此情況下,導出項目使其在當前包外可見。如果未導出,則僅在定義它的包內可見和可用。

這種外部可見性由聲明項目的首字母大寫來控制。所有聲明,如類型變量常量函數等,如果首字母大寫,則在當前包外可見。

讓我們仔細觀察以下代碼,特別注意大小寫:

greet.go
package greet

import "fmt"

var Greeting string

func Hello(name string) string {
	return fmt.Sprintf(Greeting, name)
}

此段程式碼宣告其隸屬於 greet 套件。接著宣告了兩個符號,一個名為 Greeting 的變數和一個名為 Hello 的函數。由於兩者皆以大寫字母開頭,因此均為 已匯出 並可供任何外部程式使用。如前所述,設計一個限制存取的套件將有助於更好的 API 設計,並使您能夠在內部輕鬆更新套件,而不會破壞依賴於您的套件的任何人的程式碼。

定義套件可見性

為了更深入了解套件可見性在程式中的運作方式,讓我們創建一個 logging 套件,同時考慮我們希望對外公開哪些內容以及不公開哪些內容。此日誌套件將負責將我們程式的任何訊息記錄到控制台,並會根據我們記錄的 等級 來進行判斷。等級描述了日誌的類型,將是以下三種狀態之一:資訊警告錯誤

首先,在您的 src 目錄中,創建一個名為 logging 的目錄來存放我們的日誌文件:

  1. mkdir logging

接著進入該目錄:

  1. cd logging

然後,使用 nano 等編輯器創建一個名為 logging.go 的文件:

  1. nano logging.go

將以下程式碼放入剛剛創建的 logging.go 文件中:

logging/logging.go
package logging

import (
	"fmt"
	"time"
)

var debug bool

func Debug(b bool) {
	debug = b
}

func Log(statement string) {
	if !debug {
		return
	}

	fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}

此段程式碼的第一行宣告了一個名為 logging 的套件。在這個套件中,有兩個 exported 函數:DebugLog。這些函數可以被任何導入 logging 套件的其他套件調用。此外,還有一個私有變數,名為 debug。該變數僅能在 logging 套件內部訪問。值得注意的是,儘管函數 Debug 和變數 debug 拼寫相同,但函數首字母大寫,變數則否。這使得它們成為具有不同作用域的獨立宣告。

保存並退出檔案。

要在程式碼的其他部分使用此套件,我們可以 將其 import 到一個新套件中。我們將創建這個新套件,但首先需要一個新的目錄來存放這些源文件。

讓我們離開 logging 目錄,創建一個名為 cmd 的新目錄,並進入該新目錄:

  1. cd ..
  2. mkdir cmd
  3. cd cmd

在我們剛剛創建的 cmd 目錄中,創建一個名為 main.go 的檔案:

  1. nano main.go

現在我們可以添加以下程式碼:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")
}

我們現在已經完成了整個程式的編寫。然而,在運行這個程式之前,我們還需要創建幾個配置文件,以確保代碼能夠正常運作。Go 使用 Go Modules 來配置包依賴,以便導入資源。Go 模塊是放置在包目錄中的配置文件,它告訴編譯器從哪裡導入包。雖然學習模塊的內容超出了本文的範疇,但我們只需編寫幾行配置即可使這個示例在本地運行。

打開 cmd 目錄下的 go.mod 文件:

  1. nano go.mod

然後將以下內容放入文件中:

go.mod
module github.com/gopherguides/cmd

replace github.com/gopherguides/logging => ../logging

文件的第一行告訴編譯器,cmd 包的文件路徑是 github.com/gopherguides/cmd。第二行告訴編譯器,包 github.com/gopherguides/logging 可以在本地磁盤的 ../logging 目錄中找到。

我們還需要為 logging 包創建一個 go.mod 文件。讓我們回到 logging 目錄並創建一個 go.mod 文件:

  1. cd ../logging
  2. nano go.mod

將以下內容添加到文件中:

go.mod
module github.com/gopherguides/logging

這告訴編譯器,我們創建的 logging 包實際上是 github.com/gopherguides/logging 包。這使得我們可以在 main 包中導入該包,使用我們之前編寫的以下行:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")
}

您現在應該有以下目錄結構和文件布局:

├── cmd
│   ├── go.mod
│   └── main.go
└── logging
    ├── go.mod
    └── logging.go

既然所有配置都已完成,我們可以從 cmd 套件運行 main 程式,使用以下命令:

  1. cd ../cmd
  2. go run main.go

你將得到類似以下的輸出:

Output
2019-08-28T11:36:09-05:00 This is a debug statement...

程式會印出當前時間,格式為 RFC 3339,隨後是我們傳送給記錄器的任何陳述。RFC 3339 是一種為網際網路設計的時間表示格式,常用於日誌檔案。

由於 DebugLog 函數是從記錄套件導出的,我們可以在 main 套件中使用它們。然而,logging 套件中的 debug 變數並未導出。嘗試引用未導出的宣告將導致編譯時錯誤。

main.go 中加入以下突出顯示的行:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")

	fmt.Println(logging.debug)
}

保存並運行檔案。你將收到類似以下的錯誤:

Output
. . . ./main.go:10:14: cannot refer to unexported name logging.debug

現在我們已經看到套件中 導出未導出 項目的行為,接下來我們將探討如何從 結構體 導出 欄位方法

結構體內的可見性

儘管上一節中我們構建的日誌器中的可見性方案可能適用於簡單的程式,但它共享了太多狀態,以至於無法在多個包中有效使用。這是因為導出的變數可以被多個包訪問,這些包可能會將變數修改為相互矛盾的狀態。允許以這種方式改變包的狀態,使得難以預測程式的行為。例如,在當前設計中,一個包可以將 Debug 變數設置為 true,而另一個包在同一實例中可以將其設置為 false。這將導致問題,因為兩個導入了 logging 包的包都會受到影響。

我們可以通過創建一個結構體,然後在其上掛載方法,來使日誌器隔離。這將允許我們創建一個日誌器實例,以便在每個使用它的包中獨立使用。

logging 包修改如下,以重構代碼並隔離日誌器:

logging/logging.go
package logging

import (
	"fmt"
	"time"
)

type Logger struct {
	timeFormat string
	debug      bool
}

func New(timeFormat string, debug bool) *Logger {
	return &Logger{
		timeFormat: timeFormat,
		debug:      debug,
	}
}

func (l *Logger) Log(s string) {
	if !l.debug {
		return
	}
	fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}

在這段程式碼中,我們創建了一個 Logger 結構體。這個結構體將包含我們的未導出狀態,包括要打印的時間格式和 debug 變數設置為 truefalseNew 函數設置初始狀態以創建日誌記錄器,例如時間格式和調試狀態。然後它將我們給予的值內部存儲到未導出的變數 timeFormatdebug 中。我們還在 Logger 類型上創建了一個名為 Log 的方法,該方法接受我們要打印的語句。在 Log 方法中,有一個對其本地方法變數 l 的引用,以便重新訪問其內部字段,如 l.timeFormatl.debug

這種方法將允許我們在許多不同的包中創建 Logger,並獨立於其他包的使用方式使用它。

要在另一個包中使用它,讓我們修改 cmd/main.go 使其看起來如下:

cmd/main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("This is a debug statement...")
}

運行此程序將給出以下輸出:

Output
2019-08-28T11:56:49-05:00 This is a debug statement...

在這段程式碼中,我們通過調用導出的函數 New 創建了一個日誌記錄器實例。我們將此實例的引用存儲在 logger 變數中。現在我們可以調用 logging.Log 來打印語句。

如果我們嘗試引用 Logger 中的未導出字段,例如 timeFormat 字段,我們將收到編譯時錯誤。嘗試添加以下突出顯示的行並運行 cmd/main.go

cmd/main.go

package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("This is a debug statement...")

	fmt.Println(logger.timeFormat)
}

這將給出以下錯誤:

Output
. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

編譯器認識到logger.timeFormat未被導出,因此無法從logging包中獲取。

方法內的可見性

與結構體字段類似,方法也可以被導出或未導出。

為了說明這一點,我們將為我們的日誌記錄器添加分級日誌記錄。分級日誌記錄是一種對日誌進行分類的方法,以便您可以搜索特定類型的事件。我們將添加到日誌記錄器的級別包括:

  • info級別,表示通知用戶某個操作的信息類型事件,例如程序已啟動電子郵件已發送。這些幫助我們調試和追踪程序的某些部分,以查看是否發生預期行為。

  • warning級別。這類事件標識當發生非錯誤的意外情況,例如電子郵件發送失敗,正在重試。它們幫助我們查看程序中那些不如我們預期的那樣順利運行的部分。

  • 「警告」層級,意指當發生非錯誤性質的意外情況,例如「電子郵件發送失敗,正在重試」。這些事件有助於我們發現程式中某些運作不如預期的部分。

您可能還希望開啟或關閉特定層級的日誌記錄,尤其是在程式未按預期執行且您希望進行除錯時。我們將通過修改程式,使得當 debug 設置為 true 時,會輸出所有層級的消息;否則,若為 false,則僅輸出錯誤消息來添加此功能。

通過對 logging/logging.go 進行以下修改來添加分級日誌功能:

logging/logging.go

package logging

import (
	"fmt"
	"strings"
	"time"
)

type Logger struct {
	timeFormat string
	debug      bool
}

func New(timeFormat string, debug bool) *Logger {
	return &Logger{
		timeFormat: timeFormat,
		debug:      debug,
	}
}

func (l *Logger) Log(level string, s string) {
	level = strings.ToLower(level)
	switch level {
	case "info", "warning":
		if l.debug {
			l.write(level, s)
		}
	default:
		l.write(level, s)
	}
}

func (l *Logger) write(level string, s string) {
	fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}

在這個示例中,我們為 Log 方法引入了一個新參數。現在可以傳入日誌消息的 levelLog 方法會判斷消息的層級。如果是 infowarning 消息,並且 debug 字段為 true,則會寫入消息;否則忽略該消息。如果是其他層級,例如 error,則無論如何都會輸出消息。

決定是否輸出訊息的大部分邏輯存在於Log方法中。我們還引入了一個未導出的方法,稱為writewrite方法實際上負責輸出日誌訊息。

現在我們可以通過修改cmd/main.go,使其看起來如下,從而在其他包中使用這種分級日誌記錄:

cmd/main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

}

運行此程式將得到以下結果:

Output
[info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed

在此示例中,cmd/main.go成功使用了導出的Log方法。

現在我們可以通過將debug切換為false,傳入每條訊息的level

main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, false)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

}

現在我們將看到只有error級別的訊息被打印出來:

Output
[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

如果我們嘗試從logging包外部調用write方法,將會收到編譯時錯誤:

main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

	logger.write("error", "log this message...")
}
Output
cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

當編譯器發現你試圖引用另一個包中以小寫字母開頭的內容時,它知道這是未導出的,因此會拋出一個編譯錯誤。

本教程中的日誌器展示了如何編寫僅暴露我們希望其他包使用的部分的代碼。由於我們控制了包的哪些部分對外部可見,因此現在能夠在未來進行更改而不影響依賴我們包的任何代碼。例如,如果我們希望僅在 debug 為假時關閉 info 級別的消息,您可以進行此更改而不影響 API 的其他部分。我們還可以安全地更改日誌消息以包含更多信息,例如程序運行的目錄。

結論

本文展示了如何在保護包的實現細節的同時在包之間共享代碼。這使您能夠導出一個簡單的 API,該 API 很少為了向後兼容而更改,但允許根據需要在包中進行私有更改,以使其在未來更好地工作。在創建包及其相應 API 時,這被認為是一種最佳實踐。

若想深入了解 Go 語言中的套件,可參閱我們的 Go 語言中的套件導入如何在 Go 中編寫套件 文章,或瀏覽我們整個 Go 程式設計教學系列

Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go