「每周譯Go」理解 Go 中包的可見(jiàn)性
目錄
【資料圖】
在 Go 中導(dǎo)入包
理解 Go 中包的可見(jiàn)性
如何在 Go 中編寫(xiě)條件語(yǔ)句
如何在 Go 中編寫(xiě) Switch 語(yǔ)句
如何在 Go 中構(gòu)造 for 循環(huán)
在循環(huán)中使用 Break 和 Continue
如何在 Go 中定義并調(diào)用函數(shù)
如何在 Go 中使用可變參數(shù)函數(shù)
了解 Go 中的 defer
了解 Go 中的 init
用構(gòu)建標(biāo)簽定制 Go 二進(jìn)制文件
了解 Go 中的指針
在 Go 中定義結(jié)構(gòu)體
在 Go 中定義方法
如何構(gòu)建和安裝 Go 程序
如何在 Go 中使用結(jié)構(gòu)體標(biāo)簽
如何在 Go 使用 interface
在不同的操作系統(tǒng)和架構(gòu)編譯 Go 應(yīng)用
用 ldflags 設(shè)置 Go 應(yīng)用程序的版本信息
在 Go 里面如何使用 Flag 包介紹
當(dāng)創(chuàng)建一個(gè)《Go 中的包》(點(diǎn)擊跳轉(zhuǎn)查看往期推文)時(shí),最終的目標(biāo)通常是讓其他開(kāi)發(fā)者可以使用這個(gè)包,無(wú)論是高階包還是整個(gè)程序。通過(guò)《導(dǎo)入包》(點(diǎn)擊跳轉(zhuǎn)查看往期推文),你的這段代碼可以作為其他更復(fù)雜的工具的構(gòu)建模塊。然而,只有某些包是可以導(dǎo)入的。這是由包的可見(jiàn)性決定的。
這里的可見(jiàn)性是指一個(gè)包或其他構(gòu)造可以被引用的文件空間。例如,如果我們?cè)谝粋€(gè)函數(shù)中定義一個(gè)變量,那么這個(gè)變量的可見(jiàn)性(范圍)只在定義它的那個(gè)函數(shù)中。同樣,如果你在一個(gè)包中定義了一個(gè)變量,你可以讓它只在該包中可見(jiàn),或允許它在包外也可見(jiàn)。
在編寫(xiě)符合人體工程學(xué)的代碼時(shí),仔細(xì)控制包的可見(jiàn)性是很重要的,特別是在考慮到將來(lái)可能要對(duì)你的包進(jìn)行修改時(shí)。如果你需要修復(fù)一個(gè)錯(cuò)誤,提高性能,或改變功能,你會(huì)希望以一種不會(huì)破壞使用你的包的人的代碼的方式進(jìn)行改變。盡量減少破壞性修改的一個(gè)方法是只允許訪問(wèn)你的包中需要正常使用的部分。通過(guò)限制訪問(wèn),你可以在內(nèi)部對(duì)包進(jìn)行修改,而減少影響其他開(kāi)發(fā)者使用你的包的機(jī)會(huì)。
在這篇文章中,將學(xué)習(xí)如何控制包的可見(jiàn)性,以及如何保護(hù)代碼中只應(yīng)在包內(nèi)使用的部分。為了做到這一點(diǎn),我們將創(chuàng)建一個(gè)基本的記錄器來(lái)記錄和調(diào)試信息,使用具有不同程度的項(xiàng)目可見(jiàn)性的包。
前提條件要遵循本文中的示例,你將需要:
按照《如何安裝 Go 并設(shè)置本地編程環(huán)境》(點(diǎn)擊跳轉(zhuǎn)查看往期推文)設(shè)置的 Go 工作區(qū)。本教程將使用以下文件結(jié)構(gòu):.├── bin │ └── src └── github.com └── gopherguides可導(dǎo)出與不可導(dǎo)出
不同于其他程序語(yǔ)言,如 Java 和Python使用訪問(wèn)修飾符如public、private或protected來(lái)指定范圍不同,Go 通過(guò)其聲明方式來(lái)決定一個(gè)項(xiàng)目是否exported和unxported。在這種情況下,導(dǎo)出一個(gè)項(xiàng)目會(huì)使它在當(dāng)前包之外是 "可見(jiàn)的"。如果它沒(méi)有被導(dǎo)出,它只能在它被定義的包內(nèi)可見(jiàn)和使用。
這種外部可見(jiàn)性是通過(guò)將聲明的項(xiàng)目的第一個(gè)字母大寫(xiě)來(lái)控制的。所有以大寫(xiě)字母開(kāi)頭的聲明,如 "類型"、"變量"、"常量"、"函數(shù)"等,在當(dāng)前包外是可見(jiàn)的。
讓我們看看下面的代碼,仔細(xì)注意一下大寫(xiě)字母。
packagegreetimport"fmt"varGreetingstringfuncHello(namestring)string{returnfmt.Sprintf(Greeting,name)}
這段代碼聲明它是在greet包中。然后聲明了兩個(gè)符號(hào),一個(gè)叫做 Greeting的變量和一個(gè)叫做 Hello的函數(shù)。因?yàn)樗鼈兌家源髮?xiě)字母開(kāi)頭,所以它們都被 "可導(dǎo)出" 的,可供任何外部程序使用。如前所述,精心設(shè)計(jì)一個(gè)限制訪問(wèn)的包將允許更好的 API 設(shè)計(jì),并使內(nèi)部更新你的包更容易,而不會(huì)破壞任何依賴此包的代碼。
定義包的可見(jiàn)性為了仔細(xì)看看包的可見(jiàn)性在程序中是如何工作的,讓我們創(chuàng)建一個(gè)logging包,記住哪些信息我們希望包外可見(jiàn),哪些我們不希望它可見(jiàn)。這個(gè)日志包將負(fù)責(zé)把我們程序的任何信息記錄到控制臺(tái)。它還將查看我們?cè)谑裁?em>級(jí)別上進(jìn)行的日志記錄,一個(gè)級(jí)別描述了日志的類型,它將是三種狀態(tài)之一:信息、警告或錯(cuò)誤。
首先,在你的 src目錄下,創(chuàng)建一個(gè)名為 logging的目錄來(lái)放置日志文件:
mkdirlogging
進(jìn)入目錄:
cdlogging
然后,使用 nano 這樣的編輯器,創(chuàng)建一個(gè)名為logging.go的文件:
nanologging.go
在剛剛創(chuàng)建的logging.go文件中寫(xiě)入以下代碼:
packageloggingimport("fmt""time")vardebugboolfuncDebug(bbool){debug=b}funcLog(statementstring){if!debug{return}fmt.Printf("%s%s\n",time.Now().Format(time.RFC3339),statement)}
這段代碼的第一行聲明了一個(gè)名為 logging的包。在這個(gè)包中,有兩個(gè) "導(dǎo)出 "的函數(shù)。Debug和Log。這些函數(shù)可以被任何其他導(dǎo)入logging的包所調(diào)用。還有一個(gè)名為debug的私有變量。這個(gè)變量只能從logging包內(nèi)訪問(wèn)。值得注意的是,雖然函數(shù)Debug和變量debug的拼寫(xiě)相同,但函數(shù)是大寫(xiě)的,變量不是。這使得它們成為具有不同作用域的不同聲明。
保存并退出該文件。
為了在我們代碼的其他地方使用這個(gè)包,我們可以import它到一個(gè)新的包。我們將創(chuàng)建這個(gè)新的包,但需要一個(gè)新的目錄來(lái)首先存儲(chǔ)這些源文件。
讓我們離開(kāi)logging目錄,創(chuàng)建一個(gè)名為cmd的新目錄,然后進(jìn)入這個(gè)新目錄:
cd..mkdircmdcdcmd
在剛剛創(chuàng)建的cmd目錄下創(chuàng)建一個(gè)名為main.go的文件:
nanomain.go
現(xiàn)在我們可以添加以下代碼:
packagemainimport"github.com/gopherguides/logging"funcmain(){logging.Debug(true)logging.Log("Thisisadebugstatement...")}
現(xiàn)在整個(gè)程序已經(jīng)寫(xiě)好了。然而,在運(yùn)行這個(gè)程序之前,我們還需要?jiǎng)?chuàng)建幾個(gè)配置文件,以便我們的代碼能夠正常工作。Go 使用Go 模塊來(lái)配置導(dǎo)入資源的軟件包依賴性。Go 模塊是放置在你的包目錄中的配置文件,它告訴編譯器從哪里導(dǎo)入包。雖然對(duì)模塊的學(xué)習(xí)超出了本文的范圍,但我們可以只寫(xiě)幾行配置來(lái)使這個(gè)例子在本地工作。
在cmd目錄下打開(kāi)以下go.mod文件:
nanogo.mod
然后在文件中放置以下內(nèi)容:
modulegithub.com/gopherguides/cmdreplacegithub.com/gopherguides/logging=>../logging
這個(gè)文件的第一行告訴編譯器,cmd包的文件路徑是github.com/gopherguides/cmd。第二行告訴編譯器,github.com/gopherguides/logging包可以在磁盤(pán)上的.../logging目錄下找到。
我們還需要一個(gè)go.mod文件用于我們的logging包。讓我們回到logging目錄中,創(chuàng)建一個(gè)go.mod文件。
cd../loggingnanogo.mod
在文件中加入以下內(nèi)容:
modulegithub.com/gopherguides/logging
這告訴編譯器,我們創(chuàng)建的logging包實(shí)際上是github.com/gopherguides/logging包。這使得在 main包中導(dǎo)入該包成為可能,之前寫(xiě)了以下這一行:
packagemainimport"github.com/gopherguides/logging"funcmain(){logging.Debug(true)logging.Log("Thisisadebugstatement...")}
你現(xiàn)在應(yīng)該有以下目錄結(jié)構(gòu)和文件布局:
├── cmd│ ├── go.mod│ └── main.go└── logging ├── go.mod └── logging.go
現(xiàn)在我們已經(jīng)完成了所有的配置,可以用以下命令運(yùn)行cmd包中的main程序:
cd../cmdgorunmain.go
你將得到類似以下的輸出:
2019-08-28T11:36:09-05:00 This is a debug statement...
該程序?qū)⒁?RFC 3339 格式打印出當(dāng)前時(shí)間,后面是我們發(fā)送給記錄器的任何語(yǔ)句。RFC 3339是一種時(shí)間格式,被設(shè)計(jì)用來(lái)表示互聯(lián)網(wǎng)上的時(shí)間,通常用于日志文件。
因?yàn)镈ebug和Log函數(shù)是從日志包中導(dǎo)出的,我們可以在main包中使用它們。然而,logging包中的debug變量沒(méi)有被導(dǎo)出。試圖引用一個(gè)未導(dǎo)出的聲明將導(dǎo)致一個(gè)編譯時(shí)錯(cuò)誤。
在main.go中添加錯(cuò)誤操作的一行fmt.Println(logging.debug):
packagemainimport"github.com/gopherguides/logging"funcmain(){logging.Debug(true)logging.Log("Thisisadebugstatement...")fmt.Println(logging.debug)}
保存并運(yùn)行該文件,你將收到一個(gè)類似于以下的錯(cuò)誤:
. . ../main.go:10:14: cannot refer to unexported name logging.debug
現(xiàn)在我們已經(jīng)了解了包中的 exported和 unexported項(xiàng)的行為,接下來(lái)我們將看看如何從 structs中導(dǎo)出 fields和 methods。
結(jié)構(gòu)內(nèi)的可見(jiàn)性雖然在上一節(jié)中構(gòu)建的記錄器中的可見(jiàn)性方案可能對(duì)簡(jiǎn)單的程序有效,但它分享了太多的狀態(tài),在多個(gè)包中都是有用的。這是因?yàn)閷?dǎo)出的變量可以被多個(gè)包所訪問(wèn),這些包可以將變量修改成相互矛盾的狀態(tài)。允許你的包的狀態(tài)以這種方式被改變,使得你很難預(yù)測(cè)你的程序?qū)⑷绾伪憩F(xiàn)。例如,在目前的設(shè)計(jì)中,一個(gè)包可以將Debug變量設(shè)置為true,而另一個(gè)包可以在同一實(shí)例中將其設(shè)置為false。這將產(chǎn)生一個(gè)問(wèn)題,因?yàn)閷?dǎo)入logging包的兩個(gè)包都會(huì)受到影響。
我們可以通過(guò)創(chuàng)建一個(gè)結(jié)構(gòu),然后把方法掛在它上面,使日志記錄器隔離。這將允許我們創(chuàng)建一個(gè)日志記錄器的instance實(shí)例,在每個(gè)使用它的包中獨(dú)立使用。
將logging包改為以下內(nèi)容,以重構(gòu)代碼并隔離記錄器:
packageloggingimport("fmt""time")typeLoggerstruct{timeFormatstringdebugbool}funcNew(timeFormatstring,debugbool)*Logger{return&Logger{timeFormat:timeFormat,debug:debug,}}func(l*Logger)Log(sstring){if!l.debug{return}fmt.Printf("%s%s\n",time.Now().Format(l.timeFormat),s)}
在這段代碼中,我們創(chuàng)建了一個(gè)Logger結(jié)構(gòu)。這個(gè)結(jié)構(gòu)將存放未導(dǎo)出的狀態(tài),包括要打印出來(lái)的時(shí)間格式和debug變量設(shè)置為true或false。New函數(shù)設(shè)置初始狀態(tài)來(lái)創(chuàng)建記錄器,例如時(shí)間格式和調(diào)試狀態(tài)。然后,它將內(nèi)部給它的值存儲(chǔ)到未導(dǎo)出的變量timeFormat和debug中。我們還在Logger類型上創(chuàng)建了一個(gè)名為L(zhǎng)og的方法,該方法接收我們想要打印出來(lái)的語(yǔ)句。在Log方法內(nèi)有一個(gè)對(duì)其本地方法變量l的引用,以獲得對(duì)其內(nèi)部字段的訪問(wèn),如l.timeFormat和l.debug。
這種方法將允許在許多不同的包中創(chuàng)建一個(gè)Logger,并獨(dú)立于其他包的使用方式而使用它。
為了在其他軟件包中使用它,讓我們把cmd/main.go改成下面的樣子:
packagemainimport("time""github.com/gopherguides/logging")funcmain(){logger:=logging.New(time.RFC3339,true)logger.Log("Thisisadebugstatement...")}
運(yùn)行這個(gè)程序?qū)⒔o你帶來(lái)以下輸出:
output2019-08-28T11:56:49-05:00 This is a debug statement...
在這段代碼中,我們通過(guò)調(diào)用導(dǎo)出的函數(shù)New創(chuàng)建了一個(gè)記錄器的實(shí)例。將這個(gè)實(shí)例的引用存儲(chǔ)在logger變量中?,F(xiàn)在可以調(diào)用logging.Log來(lái)打印出語(yǔ)句。
如果試圖從logger中引用一個(gè)未導(dǎo)出的字段,如timeFormat字段,將收到一個(gè)編譯時(shí)錯(cuò)誤。嘗試添加以下高亮行,并運(yùn)行cmd/main.go。
packagemainimport("time""github.com/gopherguides/logging")funcmain(){logger:=logging.New(time.RFC3339,true)logger.Log("Thisisadebugstatement...")fmt.Println(logger.timeFormat)}
這將給出如下錯(cuò)誤信息:
. . .cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
編譯器認(rèn)識(shí)到logger.timeFormat沒(méi)有被導(dǎo)出,因此不能從logging包中檢索到。
方法中的可見(jiàn)性與結(jié)構(gòu)字段相同,方法也可以被導(dǎo)出或未導(dǎo)出。
為了說(shuō)明這一點(diǎn),讓我們?yōu)槿罩酒魈砑?em>級(jí)別的日志記錄。分級(jí)日志是一種對(duì)日志進(jìn)行分類的方法,這樣就可以在日志中搜索特定類型的事件。我們將在記錄器中加入的級(jí)別是。
info級(jí)別,代表信息類型的事件,通知用戶一個(gè)動(dòng)作,如 "程序開(kāi)始",或 "電子郵件發(fā)送"。這些幫助我們調(diào)試和跟蹤我們程序的一部分,看看是否有預(yù)期的行為發(fā)生。warning級(jí)別。這些類型的事件可以識(shí)別出一些不屬于錯(cuò)誤的意外情況,如 "郵件發(fā)送失敗,重試"。它們幫助我們看到我們的程序中沒(méi)有像我們預(yù)期的那樣順利進(jìn)行的部分。error級(jí)別,意味著程序遇到了問(wèn)題,如 "未找到文件"。這往往會(huì)導(dǎo)致程序的運(yùn)行失敗。你也可能希望打開(kāi)和關(guān)閉某些級(jí)別的日志記錄,特別是當(dāng)你的程序沒(méi)有按照預(yù)期執(zhí)行,你想調(diào)試程序的時(shí)候。我們將通過(guò)改變程序來(lái)增加這個(gè)功能,當(dāng)debug被設(shè)置為true時(shí),它將打印所有級(jí)別的信息。否則,如果它是false,它將只打印錯(cuò)誤信息。
通過(guò)對(duì)logging/logging.go進(jìn)行以下修改來(lái)增加分級(jí)日志:
packageloggingimport("fmt""strings""time")typeLoggerstruct{timeFormatstringdebugbool}funcNew(timeFormatstring,debugbool)*Logger{return&Logger{timeFormat:timeFormat,debug:debug,}}func(l*Logger)Log(levelstring,sstring){level=strings.ToLower(level)switchlevel{case"info","warning":ifl.debug{l.write(level,s)}default:l.write(level,s)}}func(l*Logger)write(levelstring,sstring){fmt.Printf("[%s]%s%s\n",level,time.Now().Format(l.timeFormat),s)}
在這個(gè)例子中,我們?yōu)長(zhǎng)og方法引入了一個(gè)新的參數(shù)。我們現(xiàn)在可以傳入日志信息的級(jí)別。Log方法決定了它是什么級(jí)別的消息。如果是 info或 warning消息,并且 debug字段是 true,,那么它就會(huì)寫(xiě)下該消息。否則,它將忽略該消息。如果是其他級(jí)別的信息,比如 error,它將寫(xiě)出該信息。
大多數(shù)確定消息是否被打印出來(lái)的邏輯存在于Log方法中。我們還引入了一個(gè)未導(dǎo)出的方法,叫做 write。write方法是實(shí)際輸出日志信息的方法。
現(xiàn)在我們可以在其他軟件包中使用這種分級(jí)日志,方法是將cmd/main.go改成下面的樣子:
packagemainimport("time""github.com/gopherguides/logging")funcmain(){logger:=logging.New(time.RFC3339,true)logger.Log("info","startingupservice")logger.Log("warning","notasksfound")logger.Log("error","exiting:noworkperformed")}
運(yùn)行這個(gè)將返回:
[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
在這個(gè)例子中,cmd/main.go成功使用了導(dǎo)出的Log方法。
現(xiàn)在我們可以通過(guò)將debug切換為false來(lái)傳遞每個(gè)消息的`level":
packagemainimport("time""github.com/gopherguides/logging")funcmain(){logger:=logging.New(time.RFC3339,false)logger.Log("info","startingupservice")logger.Log("warning","notasksfound")logger.Log("error","exiting:noworkperformed")}
現(xiàn)在我們將看到,只有 error級(jí)別的信息會(huì)被打印出來(lái):
[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
如果我們?cè)噲D從logging包之外調(diào)用write方法,我們將收到一個(gè)編譯時(shí)錯(cuò)誤:
packagemainimport("time""github.com/gopherguides/logging")funcmain(){logger:=logging.New(time.RFC3339,true)logger.Log("info","startingupservice")logger.Log("warning","notasksfound")logger.Log("error","exiting:noworkperformed")logger.write("error","logthismessage...")}
cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
當(dāng)編譯器看到你試圖引用另一個(gè)包中以小寫(xiě)字母開(kāi)頭的東西時(shí),它知道這個(gè)東西沒(méi)有被導(dǎo)出,因此拋出一個(gè)編譯器錯(cuò)誤。
本教程中的記錄器說(shuō)明了如何編寫(xiě)代碼,只暴露出希望其他包消費(fèi)的部分。因?yàn)槲覀兛刂屏税哪男┎糠衷诎馐强梢?jiàn)的,所以現(xiàn)在能夠在未來(lái)進(jìn)行修改而不影響任何依賴包的代碼。例如,如果想只在debug為 false 時(shí)關(guān)閉info級(jí)別的消息,你可以在不影響你的 API 的任何其他部分的情況下做出這個(gè)改變。我們也可以安全地對(duì)日志信息進(jìn)行修改,以包括更多的信息,如程序運(yùn)行的目錄。
總結(jié)這篇文章展示了如何在包之間共享代碼,同時(shí)也保護(hù)你的包的實(shí)現(xiàn)細(xì)節(jié)。這允許你輸出一個(gè)簡(jiǎn)單的 API,為了向后兼容而很少改變,但允許在你的包中根據(jù)需要私下改變,使其在未來(lái)更好地工作。這被認(rèn)為是創(chuàng)建包和它們相應(yīng)的 API 時(shí)的最佳做法。
要了解更多關(guān)于 Go 中的包,請(qǐng)查看我們的《在 Go 中導(dǎo)入包》(點(diǎn)擊跳轉(zhuǎn)查看往期推文)和《如何在 Go 中編寫(xiě)包》(點(diǎn)擊跳轉(zhuǎn)查看往期推文)文章,或者探索我們整個(gè)《如何在 Go 中編碼系列》。
相關(guān)鏈接:
Python:https://www.digitalocean.com/community/tutorial_series/how-to-code-in-python-3Go 模塊:https://blog.golang.org/using-go-modulesRFC 3339:https://tools.ietf.org/html/rfc3339如何在 Go 中編碼系列:https://gocn.github.io/How-To-Code-in-Go/2022 GopherChina大會(huì)報(bào)名火熱進(jìn)行中!
掃描下方二維碼即可報(bào)名參與
大會(huì)合作、現(xiàn)場(chǎng)招聘及企業(yè)購(gòu)票等事宜請(qǐng)聯(lián)系微信:18516100522
關(guān)鍵詞: 進(jìn)行修改 編譯時(shí)錯(cuò)誤 以下內(nèi)容
相關(guān)閱讀
-
世界熱推薦:今晚7:00直播丨下一個(gè)突破...
今晚19:00,Cocos視頻號(hào)直播馬上點(diǎn)擊【預(yù)約】啦↓↓↓在運(yùn)營(yíng)了三年... -
NFT周刊|Magic Eden宣布支持Polygon網(wǎng)...
Block-986在NFT這樣的市場(chǎng),每周都會(huì)有相當(dāng)多項(xiàng)目起起伏伏。在過(guò)去... -
環(huán)球今亮點(diǎn)!頭條觀察 | DeFi的興衰與...
在比特幣得到機(jī)構(gòu)關(guān)注之后,許多財(cái)務(wù)專家預(yù)測(cè)世界將因?yàn)榧用茇泿诺?.. -
重新審視合作,體育Crypto的可靠關(guān)系才能雙贏
Block-987即使在體育Crypto領(lǐng)域,人們的目光仍然集中在FTX上。隨著... -
簡(jiǎn)訊:前端單元測(cè)試,更進(jìn)一步
前端測(cè)試@2022如果從2014年Jest的第一個(gè)版本發(fā)布開(kāi)始計(jì)算,前端開(kāi)發(fā)... -
焦點(diǎn)熱訊:劉強(qiáng)東這波操作秀
近日,劉強(qiáng)東發(fā)布京東全員信,信中提到:自2023年1月1日起,逐步為...