詳解Go語(yǔ)言如何實(shí)現(xiàn)類似Python中的with上下文管理器
熟悉 Python 的同學(xué)應(yīng)該知道 Python 中的上下文管理器非常好用,在對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫、訪問文件等操作時(shí),上下文管理器能夠確保資源在使用后得到釋放。在 Go 中是否也能實(shí)現(xiàn)上下文管理器呢?這便是本文所要探討的話題。
Python 上下文管理器
以操作文件為例,為了保證操作文件完成后資源能被正確關(guān)閉,在 Python 中我們可以編寫出如下代碼:
try:
f = open('foo.txt', 'r')
print(f.readlines())
finally:
f.close()不過這種寫法顯然不夠 Pythonic,Python 在語(yǔ)法層面提供了 with 語(yǔ)句實(shí)現(xiàn)上下文管理,用法如下:
with open('foo.txt', 'r') as f:
print(f.readlines())這段使用 with 語(yǔ)句實(shí)現(xiàn)的代碼,才更符合 Python 哲學(xué)。
如果你對(duì) Python with 語(yǔ)法不熟悉,可以參閱我的文章《Python 上下文管理器實(shí)現(xiàn)》。
Go 中資源釋放問題
我們知道,在 Go 語(yǔ)言中訪問數(shù)據(jù)庫(kù)、文件等資源時(shí),可以使用 defer 語(yǔ)句完成資源釋放操作。
如下定義一個(gè) ReadFile 函數(shù)用來(lái)讀取文件:
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Printf("%s content: %s\n", file.Name(), content)
}
return nil
}這個(gè)函數(shù)使用循環(huán)遍歷傳進(jìn)來(lái)的文件路徑列表,依次打開文件并輸出文件內(nèi)容。
為了保證即使在遇到錯(cuò)誤時(shí),資源也能夠被釋放,我們往往會(huì)使用 defer file.Close() 來(lái)關(guān)閉文件。
不過,這段代碼其實(shí)是存在問題的,我們知道 defer 的調(diào)用實(shí)際上并不會(huì)立即執(zhí)行,而是等到函數(shù)退出時(shí)才會(huì)執(zhí)行。
所以,代碼中的 defer 調(diào)用并不會(huì)在本輪循環(huán)中處理完當(dāng)前文件時(shí)被執(zhí)行,而是直到所有循環(huán)執(zhí)行完成,函數(shù)退出時(shí)才會(huì)執(zhí)行。
我們可以對(duì)以上示例稍作修改,來(lái)驗(yàn)證下這個(gè)問題:
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
defer func() {
file.Close()
fmt.Printf("close %s\n", file.Name())
}()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Printf("%s content: %s\n", file.Name(), content)
}
return nil
}我們將原來(lái)的 defer 語(yǔ)句改成:
defer func() {
file.Close()
fmt.Printf("close %s\n", file.Name())
}()以此來(lái)顯示 defer 調(diào)用時(shí)機(jī)。
針對(duì)以上示例,我們使用如下代碼來(lái)調(diào)用:
func main() {
err := ReadFile([]string{"foo.txt", "bar.txt"})
fmt.Printf("ReadFile err: %v\n", err)
}注意:foo.txt、bar.txt 兩個(gè)文件我已經(jīng)提前準(zhǔn)備好了,foo.txt 文件內(nèi)容為 foo,bar.txt 文件內(nèi)容為 bar。
執(zhí)行以上示例,得到如下輸出:
$ go run main.go
foo.txt content: foo
bar.txt content: bar
close bar.txt
close foo.txt
ReadFile err: <nil>
根據(jù)輸出內(nèi)容可以驗(yàn)證,defer 語(yǔ)句的調(diào)用,的確在 for 循環(huán)退出以后才開始執(zhí)行。
如果打開資源過多,而沒有及時(shí)關(guān)閉,勢(shì)必會(huì)造成資源的浪費(fèi),甚至因此而意外終止程序。
所以切記,不要在循環(huán)中使用 defer。
我們可以使用匿名函數(shù)來(lái)解決這個(gè)問題:
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
err = func() error {
defer func() {
file.Close()
fmt.Printf("close %s\n", file.Name())
}()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Printf("%s content: %s\n", file.Name(), content)
return nil
}()
if err != nil {
return err
}
}
return nil
}現(xiàn)在,將 defer 語(yǔ)句放入到一個(gè)立即執(zhí)行的匿名函數(shù)中,就可以解決問題了。
執(zhí)行以上示例,得到如下輸出:
$ go run main.go
foo.txt content: foo
close foo.txt
bar.txt content: bar
close bar.txt
ReadFile err: <nil>
可以發(fā)現(xiàn),現(xiàn)在 defer 語(yǔ)句不再是等到 for 循環(huán)退出才會(huì)執(zhí)行,而是在匿名函數(shù)退出時(shí)即可執(zhí)行。
這樣,就達(dá)到了在本輪循環(huán)中盡早釋放不再使用的文件資源的目的。
此外,為了代碼的可讀性,我們可以將匿名函數(shù)提取出來(lái),單獨(dú)封裝一個(gè)函數(shù):
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
err = processFile(file)
if err != nil {
return err
}
}
return nil
}
func processFile(file *os.File) error {
defer func() {
file.Close()
fmt.Printf("close %s\n", file.Name())
}()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Printf("%s content: %s\n", file.Name(), content)
return nil
}processFile 函數(shù)專門用來(lái)處理打開的文件,ReadFile 函數(shù)可讀性也得到了提高。
執(zhí)行以上示例,得到如下輸出:
go run main.go
foo.txt content: foo
close foo.txt
bar.txt content: bar
close bar.txt
ReadFile err: <nil>
這個(gè)輸出符合預(yù)期。
以上我們介紹了兩種方式,能夠解決 defer 語(yǔ)句延遲調(diào)用的問題。
在 Go 中實(shí)現(xiàn)上下文管理器
最近為了寫《Go 語(yǔ)言中 database/sql 是如何設(shè)計(jì)的》一文,我閱讀了下 database/sql 的源碼。在這個(gè)過程中,*sql.DB.queryDC 方法中一小段代碼激起了我的興趣:
func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) (*Rows, error) {
...
if ok {
var nvdargs []driver.NamedValue
var rowsi driver.Rows
var err error
withLock(dc, func() {
nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
if err != nil {
return
}
rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
})
...
}
...
}在 *sql.DB.queryDC 方法中有一個(gè) withLock 函數(shù)的調(diào)用,withLock 函數(shù)定義如下:
func withLock(lk sync.Locker, fn func()) {
lk.Lock()
defer lk.Unlock()
fn()
}當(dāng)看到 withLock 函數(shù)定義時(shí),我瞬間就想到了 Python 中的 with 上下文管理器。
withLock 接收一個(gè) sync.Locker 接口,定義如下:
type Locker interface {
Lock()
Unlock()
}它只有兩個(gè)方法,加鎖和釋放鎖。
withLock 能夠用于所有實(shí)現(xiàn) sync.Locker 接口的對(duì)象,在執(zhí)行 fn() 前加鎖,執(zhí)行之后釋放鎖。
這與 Python 的上下文管理器功能如出一轍,就是這么一個(gè)只有三行的小函數(shù),實(shí)現(xiàn)卻相當(dāng)精妙,真可謂短小精悍。
于是,參考 withLock 函數(shù)實(shí)現(xiàn),解決 for 循環(huán)中defer 語(yǔ)句延遲調(diào)用的問題,就有了第三種解法。
我們可以模仿 withLock 實(shí)現(xiàn)一個(gè) WithClose 函數(shù):
func WithClose(closer io.Closer, fn func()) {
defer func() {
closer.Close()
fmt.Printf("close %s\n", closer.(*os.File).Name())
}()
fn()
}WithClose 接收一個(gè) io.Closer 接口,定義如下:
type Closer interface {
Close() error
}我們可以在執(zhí)行 fn() 函數(shù)之前,使用 defer 語(yǔ)句來(lái)調(diào)用 io.Closer 的 Close 方法釋放資源。
現(xiàn)在,我們可以在 ReadFile 函數(shù)中使用這個(gè)小函數(shù)了:
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
WithClose(file, func() {
var content []byte
content, err = io.ReadAll(file)
if err != nil {
return
}
fmt.Printf("%s content: %s\n", file.Name(), content)
})
if err != nil {
return err
}
}
return nil
}這個(gè)用法同 *sql.DB.queryDC 中調(diào)用 withLock 函數(shù)一樣,并且因?yàn)殚]包的存在,我們可以拿到 WithClose 內(nèi)部執(zhí)行的 fn() 函數(shù)所產(chǎn)生的錯(cuò)誤對(duì)象。
執(zhí)行以上示例,得到如下輸出:
$ go run main.go
foo.txt content: foo
close foo.txt
bar.txt content: bar
close bar.txt
ReadFile err: <nil>
這個(gè)輸出依然符合預(yù)期。
我們可以測(cè)試下遇到錯(cuò)誤的情況,修改 main 函數(shù),調(diào)用 ReadFile 時(shí)最后傳入一個(gè)不存在的文件 baz.txt:
func main() {
err := ReadFile([]string{"foo.txt", "bar.txt", "baz.txt"})
fmt.Printf("ReadFile err: %v\n", err)
}執(zhí)行以上示例,得到如下輸出:
$ go run main.go
foo.txt content: foo
close foo.txt
bar.txt content: bar
close bar.txt
ReadFile err: open baz.txt: no such file or directory
遇到錯(cuò)誤能夠被正常捕獲。
現(xiàn)在,我們就在 Go 中實(shí)現(xiàn)類了似 Python 中的 with 上下文管理器,為解決 for 循環(huán)中defer 語(yǔ)句延遲調(diào)用的問題提供了新思路。
總結(jié)
本文靈感來(lái)自于 database/sql 源碼中的一小段代碼,為大家講解了如何在 Go 中實(shí)現(xiàn)類似 Python 中的 with 上下文管理器。
切記,不要在循環(huán)中使用 defer。為了解決這個(gè)問題,我們可以使用匿名函數(shù)、函數(shù)封裝以及 WithClose 三種方案。
希望此文能對(duì)你有所幫助。
P.S.
database/sql 源碼中的這一小段代碼,找回了我在開始用 Go 作為主力語(yǔ)言后,很久沒有在編程語(yǔ)言語(yǔ)法層面上體會(huì)過快感。相較于我最近寫的幾篇長(zhǎng)篇大論型文章,本文顯得微不足道,但我還是很樂于為這一小段代碼寫一篇文章分享出來(lái),畢竟這久違的感覺又回來(lái)了。
從把 Go 作為主力編程語(yǔ)言開始,寫代碼的思路都是“平鋪直敘”,很少思考怎么寫出更加優(yōu)雅且有趣的代碼。盡管我也分享過幾篇 Go 編程模式的文章,但相較于用 Python 作為主力編程語(yǔ)言時(shí),還是少了很多“花哨”的小技巧在里面,更多的是遵循套路的樣板代碼。
盡管 Go 語(yǔ)言的哲學(xué)更適合工程化,但 Go 代碼寫多了,有時(shí)不免會(huì)略感乏味,懷念 Python 的靈活。我無(wú)意于討論哪種編程語(yǔ)言的好壞,只是,愿在編程的道路上,你我都能找到屬于自己的樂趣所在。
以上就是詳解Go語(yǔ)言如何實(shí)現(xiàn)類似Python中的with上下文管理器的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言上下文管理器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang打包成帶圖標(biāo)的exe可執(zhí)行文件
這篇文章主要給大家介紹了關(guān)于golang打包成帶圖標(biāo)的exe可執(zhí)行文件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-06-06
golang使用json格式實(shí)現(xiàn)增刪查改的實(shí)現(xiàn)示例
這篇文章主要介紹了golang使用json格式實(shí)現(xiàn)增刪查改的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Golang實(shí)現(xiàn)延遲調(diào)用的項(xiàng)目實(shí)踐
本文主要介紹了Golang實(shí)現(xiàn)延遲調(diào)用的項(xiàng)目實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
go語(yǔ)言入門環(huán)境搭建及GoLand安裝教程詳解
這篇文章主要介紹了go語(yǔ)言入門環(huán)境搭建及GoLand安裝教程詳解,需要的朋友可以參考下2020-12-12
Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
關(guān)于golang中map使用的幾點(diǎn)注意事項(xiàng)總結(jié)(強(qiáng)烈推薦!)
map是一種無(wú)序的基于key-value的數(shù)據(jù)結(jié)構(gòu),Go語(yǔ)言中的map是引用類型,必須初始化才能使用,下面這篇文章主要給大家介紹了關(guān)于golang中map使用的幾點(diǎn)注意事項(xiàng),需要的朋友可以參考下2023-01-01
golang常用庫(kù)之gorilla/mux-http路由庫(kù)使用詳解
這篇文章主要介紹了golang常用庫(kù)之gorilla/mux-http路由庫(kù)使用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10

