聊聊反射和元編程,你學(xué)會(huì)了嗎?
Mat Ryer: 大家好,歡迎來(lái)到 Go Time。今天我們將討論反射(reflection),以及它在 Go 中的含義。我們會(huì)討論 reflect 包,它能做什么,以及一些非常有趣的使用案例,甚至包括標(biāo)準(zhǔn)庫(kù)中的一些例子。最后,我們還會(huì)對(duì)它發(fā)表一些主觀看法,毫無(wú)疑問(wèn)。
今天加入我們的是 Jaana B. Dogan。你好,Jaana。
Jaana Dogan: 你好!
Mat Ryer: 歡迎回來(lái)。你最近怎么樣?
Jaana Dogan: 很好!你呢?
Mat Ryer: 嗯,還不錯(cuò),謝謝。Jaana,如果你不介意我說(shuō)的話,你聽(tīng)起來(lái)好像不太興奮……不過(guò)別擔(dān)心,這會(huì)讓你開(kāi)心起來(lái)的。Jon Calhoun 也在這里。你好,Jon!
Jon Calhoun: 嗨,Mat。不,她看起來(lái)是在思考什么。其他人看不到我們的視頻,但她看起來(lái)是在深思。
Mat Ryer: 對(duì)啊,她在“反思”。
Jon Calhoun: 是的,她在“反思”。
Jaana Dogan: 我會(huì)告訴你們我在做什么……確實(shí)如此。
Mat Ryer: 好吧,那我們從頭開(kāi)始吧。為了那些不熟悉的人,什么是反射?reflect 包能為我們提供什么功能?
Jon Calhoun: 從高層次來(lái)看,它有點(diǎn)像元編程(meta-programming),或者說(shuō)是運(yùn)行時(shí)與代碼交互。我是這么理解的,雖然我不知道官方定義是什么,但我見(jiàn)過(guò)的所有使用它的例子都是:當(dāng)你的代碼正在運(yùn)行時(shí),你想要檢查其他代碼片段,或者查看其他內(nèi)容,找到一些關(guān)于它們的信息,或者嘗試修改它們的不同方面……所以這涉及的內(nèi)容不是在開(kāi)發(fā)時(shí)由開(kāi)發(fā)者完成的,而是在程序運(yùn)行時(shí)進(jìn)行的。
Mat Ryer: 是的,動(dòng)態(tài)語(yǔ)言經(jīng)常這樣做,對(duì)吧?比如 Ruby,還有 JavaScript。我想在 JavaScript 中,你可以在運(yùn)行時(shí)將方法添加到字符串中,幾乎可以做任何你想做的事情。它是一種非常靈活的語(yǔ)言。而 Go 是一種強(qiáng)類型語(yǔ)言,它故意不允許這樣做,但 reflect 包是一個(gè)例外。
Jon Calhoun: 正如你所說(shuō)的,動(dòng)態(tài)語(yǔ)言中幾乎不會(huì)將這種行為視為某種特別的東西……它就是語(yǔ)言的一部分。這是人們自然會(huì)做的事情。如果你曾經(jīng)使用過(guò) Ruby 或類似的語(yǔ)言,它顯得非常自然,因?yàn)槟憧吹酱蠹叶荚谶@樣做。在任何代碼庫(kù)中,這都不會(huì)顯得特別突出。但是在 Go 語(yǔ)言中,不僅你需要顯式導(dǎo)入 reflect 包,功能也非常有限。我認(rèn)為這是有意為之的,并且與 Go 想要實(shí)現(xiàn)的目標(biāo)一致。
Jaana Dogan: 我來(lái)自強(qiáng)類型背景,我本來(lái)想說(shuō)反射是類型系統(tǒng)無(wú)法作為一等公民提供的所有功能……但后來(lái)我看了一下維基百科頁(yè)面,這也是為什么我對(duì)定義感到困惑。我剛才在思考,而 Mat 以為我很難過(guò)……它說(shuō)“反射是進(jìn)程檢查、內(nèi)省和修改自身結(jié)構(gòu)和行為的能力”,所以它基本上涵蓋了一切。如果你從日常語(yǔ)言中的“反射”這個(gè)詞來(lái)理解的話,這其實(shí)是有道理的。
Mat Ryer: 是的,是的。
Jaana Dogan: 所以我認(rèn)為它不僅僅局限于那小小的一部分……我在我的思維模型中試圖過(guò)度限定它,但它其實(shí)更廣泛。它涵蓋了所有關(guān)于內(nèi)省和修改結(jié)構(gòu)與行為的內(nèi)容。
Mat Ryer: 對(duì)。其實(shí)在 Go 中進(jìn)行類型斷言(type assertion)時(shí),從某種程度上來(lái)說(shuō),這也是一種反射,對(duì)嗎?在運(yùn)行時(shí)你會(huì)說(shuō)“這是某種類型,但我們不知道具體是什么類型,所以我要斷言它是某個(gè)特定類型,如果斷言成功,我就可以執(zhí)行某些操作。”從某種程度上來(lái)說(shuō),這也算是反射,對(duì)吧?但這還是發(fā)生在編譯時(shí)的,對(duì)吧?
Jon Calhoun: 是的,編譯時(shí)你確實(shí)可以加入更多檢查……但實(shí)際的檢查,我猜必須在運(yùn)行時(shí)進(jìn)行,因?yàn)樵诰幾g時(shí)你并不知道。
Mat Ryer: 嗯,你說(shuō)得對(duì)。
Jaana Dogan: 是的,斷言是在運(yùn)行時(shí)發(fā)生的,所以你可以說(shuō)它是一種內(nèi)省操作,實(shí)際上它就是反射。但類型系統(tǒng)為我們提供了一個(gè)非常好的功能,讓我們能夠以一種更優(yōu)雅的方式實(shí)現(xiàn)它,而不是依賴于 reflect 包之類的東西。所以你可以說(shuō)“是的,這是一個(gè)反射功能”,但是它通過(guò)語(yǔ)言中的語(yǔ)法糖表現(xiàn)出來(lái)。
Mat Ryer: 對(duì),確實(shí)如此。它也提供了一些檢查機(jī)制,比如你不能進(jìn)行無(wú)效的類型斷言,編譯器在某些時(shí)候會(huì)幫你做一些檢查。但你說(shuō)得對(duì),這確實(shí)是在運(yùn)行時(shí)完成的,這也正是它的目的。
Jon Calhoun: 是的,從這個(gè)角度來(lái)看確實(shí)很有趣。類型斷言可能是大家最常見(jiàn)的反射使用方式;我想第二種最常見(jiàn)的應(yīng)該是結(jié)構(gòu)體標(biāo)簽(struct tags)。雖然大家可能并不會(huì)直接使用它們,但我認(rèn)為大多數(shù) Go 開(kāi)發(fā)者至少見(jiàn)過(guò)結(jié)構(gòu)體標(biāo)簽,并且可能會(huì)想“這是什么東西?” 所以我覺(jué)得這是另一個(gè)可以深入討論的點(diǎn),因?yàn)槲艺J(rèn)為這是反射在 Go 中的第二大使用場(chǎng)景。
Mat Ryer: 沒(méi)錯(cuò)。對(duì)于那些不熟悉的人來(lái)說(shuō)---尤其是當(dāng)你處理 JSON 數(shù)據(jù)時(shí),你會(huì)看到這個(gè)現(xiàn)象……你可以在結(jié)構(gòu)體字段名后面加一個(gè)字符串,這個(gè)字符串可以在運(yùn)行時(shí)解析,當(dāng)然可以從中提取元數(shù)據(jù)。以 JSON 為例,它允許你指定字段名,這樣你可以使用與結(jié)構(gòu)體字段不同的字段名。你還可以選擇不包含某個(gè)字段。還有一種特殊的語(yǔ)法,是一個(gè)字符串加逗號(hào),這其實(shí)是 Go 語(yǔ)言中一個(gè)比較奇怪的部分,確實(shí)比較獨(dú)特。你還可以告訴它如果字段為空,則省略該字段。如果是默認(rèn)值,它就不會(huì)包含在 JSON 對(duì)象中。
我記得我第一次看到這個(gè)時(shí)……當(dāng)時(shí)感覺(jué)這可能是個(gè)臨時(shí)的功能,但事實(shí)證明它非常有用,尤其是在這種情況下非常有效。不過(guò) Jon,你寫過(guò)一個(gè)使用結(jié)構(gòu)體標(biāo)簽的項(xiàng)目,對(duì)吧?就是那個(gè)表單項(xiàng)目。
Jon Calhoun: 是的。
Mat Ryer: 那是什么?
Jon Calhoun: 我做過(guò)一些不同的項(xiàng)目……歷史上我在很多項(xiàng)目中都使用過(guò)反射。我來(lái)自 Rails 背景,而---Rails 本質(zhì)上就是一個(gè)大量使用反射的框架。我對(duì)那個(gè)框架的整體看法就是如此。所以在 Go 中我沒(méi)有做得那么復(fù)雜,因?yàn)槲矣X(jué)得在 Go 中那樣做并不合適。但我當(dāng)時(shí)想寫一些代碼,基本上我想要將一個(gè)結(jié)構(gòu)體生成一個(gè) HTML 表單,并且當(dāng)用戶提交該表單時(shí),我希望能夠解析表單內(nèi)容,并將用戶提交的所有值重新放入該結(jié)構(gòu)體中。這樣可以簡(jiǎn)化我的工作,我可以在多個(gè)處理器之間共享這個(gè)表單,并簡(jiǎn)化處理流程。
所以我創(chuàng)建了一個(gè)使用結(jié)構(gòu)體標(biāo)簽的 Form 包……當(dāng)然還有其他方法可以處理這個(gè)問(wèn)題,我們應(yīng)該討論一下……但當(dāng)時(shí)我只是想看看能不能通過(guò)這種方式處理問(wèn)題。結(jié)構(gòu)體標(biāo)簽用于一些地方,比如你需要更改字段名。如果你的結(jié)構(gòu)體字段名是“Email”,但你希望它在表單中顯示為“e_mail”,你可以使用結(jié)構(gòu)體標(biāo)簽來(lái)更改這些信息。這就是我當(dāng)時(shí)使用它的地方。但這個(gè)項(xiàng)目也是一個(gè)有趣的實(shí)踐,因?yàn)樗故玖嗽?Go 中編寫和使用反射是多么令人困惑。
我認(rèn)為很多人都會(huì)在這方面遇到困難。某種程度上我覺(jué)得這是有意為之的;他們并不是想讓它變得更糟,而是不想讓它變得那么容易,以至于人們?cè)诓槐匾那闆r下就使用反射。
Mat Ryer: 是啊……因?yàn)轭愋桶踩詭?lái)了很多好處,這樣做是有道理的,不是嗎?
Jon Calhoun: 是的。當(dāng)時(shí)我做這個(gè)項(xiàng)目時(shí),可能在使用結(jié)構(gòu)體標(biāo)簽這方面有點(diǎn)過(guò)頭了。比如,對(duì)于輸入字段的幫助文本或默認(rèn)值等,我實(shí)際上讓你可以通過(guò)結(jié)構(gòu)體標(biāo)簽來(lái)提供這些值……結(jié)果是,你可能會(huì)有一個(gè)非常長(zhǎng)的結(jié)構(gòu)體標(biāo)簽,附加在某個(gè)字段上……看起來(lái)有點(diǎn)怪,因?yàn)檫@不是真正的代碼,而是元數(shù)據(jù)。然而,它提供的功能遠(yuǎn)遠(yuǎn)超過(guò)你初看時(shí)的感覺(jué)。
Jaana Dogan: Mat,你剛才說(shuō)的很有趣---你第一次看到它時(shí),覺(jué)得它幾乎像是一個(gè)臨時(shí)的解決方案。我當(dāng)時(shí)也是這么覺(jué)得,因?yàn)槲乙灿羞@些顧慮……比如,Go 是一門非常強(qiáng)類型、簡(jiǎn)單的語(yǔ)言,但有時(shí)我會(huì)覺(jué)得自己過(guò)度使用了結(jié)構(gòu)體標(biāo)簽……我當(dāng)時(shí)還期待一種類似注解的東西;在其他語(yǔ)言中,我們有注解,注解可以是有類型的,它們可以處理更復(fù)雜的情況,而不會(huì)犧牲太多類型安全。我本以為 Go 會(huì)有類似的東西,這是很久以前---語(yǔ)言誕生之初的想法……但他們希望保持語(yǔ)言的簡(jiǎn)潔,因此沒(méi)有引入注解。我意識(shí)到的是,我并沒(méi)有看到太多由于結(jié)構(gòu)體標(biāo)簽引發(fā)的混亂。
我覺(jué)得大家通常只在非常特定的情況下使用結(jié)構(gòu)體標(biāo)簽,比如定義 JSON 鍵名之類的。那么你怎么看?你覺(jué)得目前的情況足夠了嗎?我們其實(shí)不需要注解,還是說(shuō)這是一個(gè)錯(cuò)失的機(jī)會(huì)?因?yàn)榻Y(jié)構(gòu)體標(biāo)簽難以維護(hù),我們?cè)谶@方面沒(méi)有做得很好,或者我們錯(cuò)失了一些通過(guò)更豐富的方式來(lái)注解字段的機(jī)會(huì)?
Mat Ryer: 是的,這是一個(gè)非常有趣的問(wèn)題,因?yàn)樵诮Y(jié)構(gòu)體中為特定用途添加一些額外的元數(shù)據(jù)確實(shí)有其價(jià)值。另一種選擇是直接用強(qiáng)類型來(lái)描述同樣的東西。Jon,舉個(gè)例子,你可能會(huì)有一個(gè)地址結(jié)構(gòu)體,里面有不同的字段,然后你通過(guò)結(jié)構(gòu)體標(biāo)簽給它們添加標(biāo)簽、占位符和幫助文本等信息。你可能會(huì)有一個(gè)表單類型和字段類型,這樣寫起來(lái)雖然很冗長(zhǎng),但非常清晰,這是它的優(yōu)點(diǎn)。不過(guò)我聽(tīng)說(shuō)---其實(shí)我不太確定---結(jié)構(gòu)體標(biāo)簽的解析速度很慢。這還是一個(gè)問(wèn)題嗎?有沒(méi)有對(duì)它進(jìn)行過(guò)優(yōu)化,還是說(shuō)它其實(shí)已經(jīng)很快了?
Jon Calhoun: 我不確定,但我從來(lái)沒(méi)有在一個(gè)項(xiàng)目中遇到這種速度問(wèn)題。如果我正在渲染 HTML 并將其發(fā)送給用戶,那么發(fā)送 HTML 所花費(fèi)的時(shí)間幾乎肯定會(huì)遠(yuǎn)遠(yuǎn)超過(guò)解析結(jié)構(gòu)體標(biāo)簽的時(shí)間,所以這并不是一個(gè)主要的擔(dān)憂。
我還想說(shuō),你剛才提到的表單類型---我需要把它整理成一個(gè)代碼片段并分享一下,也許我會(huì)把它放到節(jié)目的備注里。其實(shí)我有兩個(gè)版本的實(shí)現(xiàn)。一個(gè)是用表單包實(shí)現(xiàn)的,它接收一個(gè)結(jié)構(gòu)體,并生成一些 HTML,如果你提供一個(gè) HTML 模板的話……而另一個(gè)版本則是你描述一個(gè)表單類型。我有另一個(gè)結(jié)構(gòu)體,比如說(shuō)“這是我的注冊(cè)表單結(jié)構(gòu)體”,但我會(huì)為它寫一個(gè)方法,使用我通用的表單類型,然后生成它應(yīng)該是什么樣子……我知道如何使用模板來(lái)渲染它的 HTML。所以在這個(gè)版本中,我完全沒(méi)有使用反射……你說(shuō)得對(duì),這個(gè)版本確實(shí)更冗長(zhǎng),但在某些方面它肯定更好,因?yàn)樗逦乇砻髁税l(fā)生了什么。
不過(guò)有時(shí)候這也取決于項(xiàng)目的類型……因?yàn)閷?duì)于一些快速的項(xiàng)目,你只是想快速生成一個(gè)表單,這時(shí)候有一個(gè)“這個(gè)包可以直接處理”的功能是很不錯(cuò)的。而在其他情況下,如果這是一個(gè)需要長(zhǎng)期維護(hù)的項(xiàng)目,我們可能需要對(duì)內(nèi)容進(jìn)行更多的定制。這時(shí),選擇一個(gè)更容易修改、更冗長(zhǎng)的方式可能更有意義,但最終你還是能得到相同的結(jié)果。
Mat Ryer: 是的,我記得 App Engine 舊的數(shù)據(jù)存儲(chǔ)也使用過(guò)它們……通常是用于字段名稱,但你也可以指定不希望在某個(gè)字段上建立索引,然后將其放入數(shù)據(jù)存儲(chǔ)中。能夠以這種方式注解結(jié)構(gòu)體是非常強(qiáng)大的,這也很合理,因?yàn)槟愦_實(shí)是在討論該字段的屬性,這是非常直接的。所以是的……還有你提到的類型化注解---我記得 C# 中有這個(gè)功能,我想 Java 也有。
這種想法是,你在代碼中有實(shí)際的類型,你可以使用這些類型來(lái)注解字段。然后,我想你可以檢查這些類型的存在,甚至可以對(duì)它們進(jìn)行程序化處理。這是一種非常酷的元編程方式,同時(shí)你可能還能保持較高的類型安全性。
Jaana Dogan: 是的,維護(hù)性也更高。你還可以輕松運(yùn)行查詢。你可以讓編輯器展示“顯示所有使用了此注解的地方”。或者假設(shè)你想修改注解中的某個(gè)值,你可以輕松搜索到它,然后在所有相關(guān)地方進(jìn)行重構(gòu)。所以擁有一定的類型安全性可以讓你做到這些……但正如我所說(shuō),我不認(rèn)為我們?cè)?Go 中過(guò)度使用結(jié)構(gòu)體標(biāo)簽。也許是因?yàn)樗鼈儧](méi)有類型,所以大家都小心翼翼地不去濫用它們……我覺(jué)得我們目前保持了一個(gè)不錯(cuò)的平衡,它們被使用得很少。但最大的問(wèn)題是,由于它們是非結(jié)構(gòu)化的,并且需要解析,維護(hù)性和潛在的性能問(wèn)題讓人擔(dān)憂。
Jon Calhoun: 我完全同意 Jaana 的看法,可能正是因?yàn)樗鼈冸y以維護(hù),人們才不常使用它們……而如果我們引入了類型化的注解,我?guī)缀蹩梢钥隙ㄈ藗儠?huì)比現(xiàn)在更頻繁地使用它們,甚至在一些不適合的場(chǎng)景中也使用它們……因?yàn)槲乙?jiàn)過(guò)類似的情況,特別是在使用結(jié)構(gòu)體標(biāo)簽時(shí)。我認(rèn)為有一類問(wèn)題非常適合使用結(jié)構(gòu)體標(biāo)簽。比如編碼 JSON,或者幾乎任何類似的編碼過(guò)程……編碼是一個(gè)很好的例子,因?yàn)槟愕慕Y(jié)構(gòu)體可能與實(shí)際需要編碼的格式不完全匹配,所以你需要有一種方式來(lái)定義它應(yīng)該如何編碼和解碼。而 ORM 也是類似的,當(dāng)你構(gòu)建一個(gè) ORM 時(shí),你可能只想快速地將數(shù)據(jù)插入 SQL 數(shù)據(jù)庫(kù)中,并指定 SQL 數(shù)據(jù)庫(kù)中的字段名稱---這很合理。
但還有其他一些庫(kù),比如驗(yàn)證庫(kù),你會(huì)在結(jié)構(gòu)體標(biāo)簽中添加類似“此字段為必填”的信息---我并不是說(shuō)人們不應(yīng)該使用這些庫(kù),但我確實(shí)認(rèn)為這些庫(kù)在長(zhǎng)期使用中可能會(huì)帶來(lái)問(wèn)題。它們可能會(huì)導(dǎo)致代碼中充滿各種結(jié)構(gòu)體標(biāo)簽,整個(gè)結(jié)構(gòu)體類型變得難以理解,維護(hù)起來(lái)也很困難。而且沒(méi)有編譯器的安全保障。如果我們加入了類型化的注解,我想人們可能會(huì)更傾向于使用它們,而不會(huì)考慮其他方法。
Mat Ryer: 是的,我見(jiàn)過(guò)一些檢查 JSON 標(biāo)簽的 linter。如果你漏掉了一個(gè)引號(hào),或者標(biāo)簽的格式不正確,有些 linter 會(huì)提醒你“哦,這個(gè)標(biāo)簽格式不正確”。雖然不是編譯器做的檢查,因此沒(méi)有同樣的安全性,但我想這種不太吸引人的 API 可能也是它不被廣泛使用的原因之一。此外,它確實(shí)有點(diǎn)神秘。對(duì)我來(lái)說(shuō),特別是我聽(tīng)到很多人說(shuō),吸引他們使用 Go 的一個(gè)原因是它沒(méi)有太多“魔法”---它是一門非常清晰簡(jiǎn)單的語(yǔ)言。現(xiàn)在,我可能有點(diǎn)走向了另一個(gè)極端,幾乎對(duì)任何神秘的東西都過(guò)敏,盡管有些人告訴我,我的外表看起來(lái)像個(gè)魔術(shù)師(笑)。
Jon Calhoun: 是的,神秘的部分確實(shí)讓人頭疼……我記得第一次使用結(jié)構(gòu)體標(biāo)簽時(shí),我在學(xué)習(xí) Go。我當(dāng)時(shí)在做與 MongoDB 相關(guān)的事情……你會(huì)用 Bison 來(lái)定義結(jié)構(gòu)體標(biāo)簽……當(dāng)時(shí)我在設(shè)置結(jié)構(gòu)體標(biāo)簽,腦子里在想“我需要導(dǎo)入什么包才能讓它工作嗎?為什么我的代碼沒(méi)有導(dǎo)入任何東西卻還能正常工作?”
Mat Ryer: 哦,是的。
Jon Calhoun: 當(dāng)時(shí)這讓我非常困惑,因?yàn)槲倚南搿拔也幻靼走@段代碼是怎么編譯通過(guò)的。”直到后來(lái)我深入研究了一下,才明白過(guò)來(lái),但當(dāng)時(shí)真的覺(jué)得這像是某種魔法,剛開(kāi)始學(xué)習(xí)時(shí)這讓我有點(diǎn)沮喪……因?yàn)槲也恢腊l(fā)生了什么。
Mat Ryer: 其實(shí)它就是一個(gè)字符串,對(duì)吧?
Jon Calhoun: 是的,但你會(huì)想“肯定是編譯器在做什么事情吧?它肯定是在某個(gè)地方寫了些什么東西”,所以你會(huì)想“這到底是怎么回事?”這讓我困惑了好一陣子。直到后來(lái)我意識(shí)到“哦,他們只是解析這個(gè)字符串,Bison 包在使用時(shí)才會(huì)處理它”,這樣就說(shuō)得通了。但當(dāng)時(shí)我真的很困惑。
Mat Ryer: 是啊。其實(shí) reflect API 對(duì)結(jié)構(gòu)體標(biāo)簽的解析還不錯(cuò),API 很簡(jiǎn)單。因?yàn)?reflect 包中的一些功能---它實(shí)在是太“元”了。有些功能你可以理解,比如你可以獲取某個(gè)值,而這個(gè)值是一個(gè)結(jié)構(gòu)體。在 reflect 包中,它是一個(gè)強(qiáng)類型的值,這個(gè)值描述了這個(gè)值的類型。然后由于這些值可以是許多不同類型的東西,你會(huì)看到很多方法,其中大多數(shù)情況下這些方法是非法調(diào)用的。
比如你試圖獲取一個(gè)整數(shù)的長(zhǎng)度,當(dāng)然,reflect 包中有這些方法可以做到這一點(diǎn)。所以在編譯時(shí)你可以調(diào)用它,但只有在運(yùn)行時(shí)你才會(huì)發(fā)現(xiàn)你不能獲取一個(gè)整數(shù)的長(zhǎng)度。類似的例子還有很多,因此你會(huì)檢查所有東西。當(dāng)你寫防御性代碼時(shí)會(huì)非常冗長(zhǎng),以確保你不會(huì)遇到任何這些運(yùn)行時(shí)的奇怪問(wèn)題。當(dāng)然,測(cè)試也能幫助你避免這些問(wèn)題,但……
Jaana Dogan: 是的,你提到了測(cè)試,但這其實(shí)也很難測(cè)試。沒(méi)有一組標(biāo)準(zhǔn)的測(cè)試用例。我曾在一些數(shù)據(jù)庫(kù)包上工作過(guò),由于 Go 沒(méi)有泛型---也許我們可以在這個(gè)對(duì)話的背景下討論一下這個(gè)問(wèn)題---我們通常會(huì)大量依賴接口和類型轉(zhuǎn)換。如果你有一個(gè)接口切片,它可以是一個(gè)值或一個(gè)指針,或者是指針的指針,然后你必須通過(guò) reflect 包來(lái)處理所有這些魔法,而 reflect 包本身已經(jīng)非常冗長(zhǎng)了,所以包裝和解包所有這些類型非常困難。我找不到一個(gè)簡(jiǎn)單的測(cè)試方法,因?yàn)闆](méi)有一組標(biāo)準(zhǔn)的測(cè)試用例。比如,如果標(biāo)準(zhǔn)庫(kù)提供了一些類似“嘿,請(qǐng)考慮測(cè)試這些”的東西,或者提供了一份測(cè)試的清單,那將會(huì)簡(jiǎn)單得多。
Mat Ryer: 是啊……因?yàn)槟憧赡苄枰獪y(cè)試所有不同的類型之類的東西……當(dāng)然,還有數(shù)組和切片。
Jaana Dogan: 沒(méi)錯(cuò)。
Jon Calhoun: 很有趣的是……Mat,你提到的一個(gè)比較簡(jiǎn)單的用例是獲取某個(gè)值……有趣的是,這是我第一次使用 reflect 庫(kù)時(shí)遇到的問(wèn)題之一。因?yàn)楫?dāng)有人傳遞了一個(gè)值,比如一個(gè)字符串,你會(huì)想“好吧,我要獲取這個(gè)值。”這很合理,你的代碼也能正常運(yùn)行,一切看起來(lái)都沒(méi)問(wèn)題。是的,像長(zhǎng)度這樣的東西可能不適用,但大多數(shù)情況下會(huì)正常工作。但當(dāng)有一天有人傳遞了一個(gè) nil 指針,它有一個(gè)類型,但它是一個(gè) nil 指針,你的代碼突然就崩潰了,你會(huì)想“剛剛發(fā)生了什么?” 你就會(huì)遇到這些奇怪的情況,如果類型是指針,并且它是 nil,那么你需要使用 reflect.new 來(lái)實(shí)例化一個(gè)新元素。如果它是一個(gè)接口,你需要獲取它所指向的底層元素類型,因?yàn)榻涌诒旧聿](méi)有多大幫助……
會(huì)有很多這些奇怪的情況,表面上看起來(lái)很簡(jiǎn)單,比如“我只是想獲取這個(gè)值”,但實(shí)際上并不是那么簡(jiǎn)單。所以你最終會(huì)遇到很多邊緣情況……即使你把它搞定了,并為這些情況寫了測(cè)試,比如傳遞了一個(gè)空指針,它有一個(gè)類型。我們傳遞了一個(gè)空接口,我們傳遞了設(shè)置了實(shí)際值的接口---你為所有這些情況寫了測(cè)試,但到最后你仍然在想“我是不是還遺漏了某些邊緣情況”,因?yàn)閹缀醪豢赡軟](méi)有遺漏。
Mat Ryer: 是的,從某種意義上說(shuō),這就像是在泄露 Go 內(nèi)部的工作原理。如果你使用 reflect,你確實(shí)會(huì)學(xué)到很多關(guān)于類型系統(tǒng)的東西……但坦白說(shuō),我經(jīng)常處于一種“試錯(cuò)”的狀態(tài),依賴 TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))來(lái)告訴我是否做對(duì)了。如果我使用了 reflect 包,我通常會(huì)寫一些代碼,比如調(diào)用 Elem() 獲取元素,然后出于某種原因(我不確定是什么),我必須再次調(diào)用 Elem()……我知道這里面一定有一個(gè)很好的解釋,但我不知道是什么解釋,我也沒(méi)時(shí)間去深究,我只知道如果我調(diào)用 elem.Elem(),在這種情況下我就能得到我需要的東西,因?yàn)闇y(cè)試通過(guò)了……所以當(dāng)涉及到反射代碼時(shí),我往往采取一種非常蠻力的方式,這種感覺(jué)不太好。
Jon Calhoun: 是的。我不常使用 TDD,但使用 reflect 時(shí),這是我?guī)缀跻欢〞?huì)使用 TDD 的情況,因?yàn)槲視?huì)想“這是我知道我要傳入的所有不同類型,它們都需要正常工作”,從這開(kāi)始會(huì)輕松得多。否則你會(huì)想“是的,這起作用了”,然后你運(yùn)行它,結(jié)果什么都不工作,你會(huì)想“我不知道發(fā)生了什么。”
我剛剛看了一些我使用 reflect 寫的代碼,我看到同樣的情況,就是 .type.Elem(),然后你創(chuàng)建了一個(gè) reflect.New(),使用了那個(gè)類型,然后又調(diào)用了 .Elem(),你會(huì)想“看著這段代碼,我完全不知道為什么我要這么做。我只知道它能工作”,這感覺(jué)真的很奇怪。
Jaana Dogan: 有一件事我意識(shí)到的是,我覺(jué)得 Go 當(dāng)前的類型系統(tǒng)在一定程度上加劇了這些問(wèn)題……因?yàn)槲覀儾坏貌灰蕾嚱涌谧鳛閰?shù),或者接口切片作為參數(shù),然后就會(huì)出現(xiàn)大量的類型轉(zhuǎn)換問(wèn)題。我們無(wú)法限制用戶想要做什么,或者用戶想要傳遞什么……你必須處理所有這些情況,才能讓你的庫(kù)正常工作。
Go 中的一個(gè)典型例子是,我們有一些庫(kù)會(huì)根據(jù)用戶傳入的參數(shù)進(jìn)行類型轉(zhuǎn)換,比如用戶傳入了某個(gè)接口類型的值……它可能是一個(gè)結(jié)構(gòu)體,也可能是一個(gè)指向結(jié)構(gòu)體的指針,或者是一個(gè)數(shù)組之類的東西,但它必須通過(guò)類型轉(zhuǎn)換來(lái)了解類型,因此你不能傳遞一個(gè)普通的 nil,而是必須傳遞一個(gè)有類型的 nil。所以 Go 有一些奇怪的地方,加上沒(méi)有泛型,這就導(dǎo)致所有這些復(fù)雜的情況需要由庫(kù)通過(guò) reflect 包來(lái)處理,我認(rèn)為這加劇了我們所經(jīng)歷的所有這些 .Elem() 的問(wèn)題,而我們并不完全理解它們的原因……整個(gè)語(yǔ)言在某種程度上加劇了這個(gè)問(wèn)題。
Jon Calhoun: 你提到的這一點(diǎn)很好---如果你在使用 reflect,你幾乎總是要接受空接口作為參數(shù)。這幾乎總是你的參數(shù)類型,而這通常是編寫代碼時(shí)的一個(gè)不好的信號(hào)。
Mat Ryer: 是的,但就像你說(shuō)的,在某些情況下這是無(wú)法避免的……
Jon Calhoun: 是的。
Mat Ryer: 我們很多人每天都在使用的一個(gè)東西就是 JSON 的編解碼功能。你可以傳遞任何類型,因?yàn)樗梢越獯a為你編寫的結(jié)構(gòu)體類型……或者更常見(jiàn)的是解碼為一個(gè) map[string]interface{}。它完全沒(méi)問(wèn)題。而且,reflect 包也可以實(shí)例化東西,對(duì)吧?如果你傳遞了一個(gè) map,它會(huì)為你創(chuàng)建 map……類似的事情。所以它確實(shí)變得有點(diǎn)奇怪。我記得以前我想寫一個(gè)用于 testify 的 mock 庫(kù),當(dāng)時(shí)我真的很想在運(yùn)行時(shí)從接口或另一個(gè)結(jié)構(gòu)體創(chuàng)建一個(gè) mock 結(jié)構(gòu)體。當(dāng)時(shí)你做不到,但從那以后我見(jiàn)過(guò)---我不確定現(xiàn)在是否可以,但我見(jiàn)過(guò)一些函數(shù)和方法,似乎現(xiàn)在你可以實(shí)例化結(jié)構(gòu)體之類的東西;我得再確認(rèn)一下……但這是非常強(qiáng)大的。如果你考慮到我們沒(méi)有泛型,確實(shí)很有誘惑力去看看能不能通過(guò) reflect 來(lái)完成這項(xiàng)艱難的工作,這樣你就能得到一個(gè)非常智能的動(dòng)態(tài)功能……這將非常有趣。
在測(cè)試代碼中,你也許可以容忍它不是那么高效---它不會(huì)出現(xiàn)在一個(gè)緊密的循環(huán)中;當(dāng)然,你不希望測(cè)試代碼變得慢。但測(cè)試代碼并不是總是處于低延遲的場(chǎng)景中,盡管我們?nèi)匀幌M麥y(cè)試代碼運(yùn)行得相對(duì)較快……
Jon Calhoun: 是的,就像 Jaana 說(shuō)的,Go 的類型系統(tǒng)有它的局限性,而你提到了 JSON 編碼……我在想,即使在你知道必須傳遞指針的情況下,你也不能只傳遞結(jié)構(gòu)體;你必須傳遞指向結(jié)構(gòu)體的指針來(lái)獲取返回值……如果有一個(gè)類型系統(tǒng)可以讓你限制這種情況,那會(huì)更好,但由于現(xiàn)有的設(shè)置方式,它做不到。相反,你必須依賴錯(cuò)誤處理之類的東西……這并不是說(shuō) Go 是個(gè)糟糕的語(yǔ)言,只是有時(shí)你會(huì)感到掙扎,特別是在看到這些限制時(shí),我相信這對(duì)一些人來(lái)說(shuō)是很困惑的。
Mat Ryer: 是的。如果 JSON 包不使用 reflect,它會(huì)是什么樣的呢?你幾乎肯定會(huì)有某種回調(diào)機(jī)制,但你仍然會(huì)有接口,因?yàn)槟悴恢乐档念愋汀?/p>
Jon Calhoun: 是的,幾乎必須是類似于“編碼這個(gè)”的方式,然后不是說(shuō)“傳入一個(gè)接口”,而是必須說(shuō)“它必須是一個(gè)指針”。必須是類似這樣的東西。不過(guò)即使這樣,也有點(diǎn)讓人困惑,因?yàn)閙ap并不總是這樣工作的,如果我沒(méi)記錯(cuò)的話。我記得你可以直接傳入一個(gè) map,而不一定非得是一個(gè)指針,但我不太記得了……它必須是一個(gè)指針嗎?
Jaana Dogan: 是的……
Jon Calhoun: 我已經(jīng)很久沒(méi)有往那里面?zhèn)魅?map 了……我應(yīng)該去檢查一下。
Mat Ryer: 哦,不……那你都傳入了什么?
Jon Calhoun: 結(jié)構(gòu)體……
Mat Ryer: 哦,沒(méi)錯(cuò)。那很合理。
Jon Calhoun: 我大多數(shù)時(shí)候都是解碼到結(jié)構(gòu)體中。
Mat Ryer: 嗯,如果你寫的東西是你不知道數(shù)據(jù)結(jié)構(gòu)的---你知道,很多 API 確實(shí)是這樣做的。
Jon Calhoun: 是的。
Mat Ryer: 這確實(shí)是個(gè)危險(xiǎn)的領(lǐng)域。但如果你完全不知道實(shí)際的類型……我寫過(guò)一個(gè)小工具---它還沒(méi)完成,雖然它大體上能工作,但絕對(duì)還沒(méi)準(zhǔn)備好……它基本上是一個(gè)假的 JSON 數(shù)據(jù)生成器。所以你可以傳入任何數(shù)據(jù)---實(shí)際上,你可以傳入一個(gè)結(jié)構(gòu)體,它會(huì)生成很多該結(jié)構(gòu)體的示例,它使用 JSON 來(lái)實(shí)現(xiàn),因?yàn)橹辽僭?API 里,JSON 的編組和解組是非常簡(jiǎn)單的事情。所以在那種情況下,是的。如果這是一個(gè)托管在網(wǎng)站上的 API,你會(huì)希望人們能夠傳入任何種類的 JSON,包括對(duì)象數(shù)組以及單個(gè)對(duì)象……然后它可以根據(jù)這些數(shù)據(jù)生成一些測(cè)試示例數(shù)據(jù)。這就是這個(gè)想法。這個(gè)工具非常“元”。
所以這種用例并不常見(jiàn),我想……但我認(rèn)為 JSON API 在某種程度上非常好用,特別是作為一個(gè)用戶。如果沒(méi)有 reflect,你最終會(huì)得到一個(gè)函數(shù),它給出的鍵是字符串,值可能是一些字節(jié),然后你必須根據(jù)你對(duì)具體情況的了解來(lái)解組這些字節(jié)。所以標(biāo)準(zhǔn)庫(kù)為我們做這些事情確實(shí)很好。順便說(shuō)一句,如果沒(méi)有這些功能,我認(rèn)為這會(huì)損害 Go 的聲譽(yù)。想象一下,如果有篇 Hacker News 的文章說(shuō)你必須手動(dòng)處理 JSON 的編組和解組……
Jaana Dogan: 是的,如果是那樣,Go 可能不會(huì)被如此廣泛地采用。
Mat Ryer: 沒(méi)錯(cuò),絕對(duì)是這樣的。
Jon Calhoun: 即使按照現(xiàn)在的方式,JSON 仍然有一些難點(diǎn)……就像你說(shuō)的,你有結(jié)構(gòu)體,但---我覺(jué)得 Stripe 是個(gè)例子,支付來(lái)源可以是信用卡或銀行賬戶,所以你會(huì)有一個(gè)可以不同的對(duì)象數(shù)組,你需要自己寫一個(gè)類型來(lái)正確地解組它……所以你不得不為此寫一些自定義的代碼。我想如果你根本沒(méi)有 JSON 包,那將是一場(chǎng)噩夢(mèng),充滿了人們的抱怨和“這太糟糕了”的評(píng)論。即使在這種情況下,當(dāng)你不得不寫自定義代碼,我仍然盡量利用 JSON 包的功能……比如,創(chuàng)建一個(gè)只包含我想要的字段的結(jié)構(gòu)體,解組它,弄清楚它是什么,然后傳入相應(yīng)類型的結(jié)構(gòu)體……這讓我省去了自己處理“如何解組”的實(shí)際開(kāi)銷。
Jon Calhoun: 剛才,Mat,你提到了結(jié)構(gòu)體標(biāo)簽,Jaana 和我在節(jié)目開(kāi)始前討論了一下。我覺(jué)得結(jié)構(gòu)體標(biāo)簽如果能成為一個(gè)單獨(dú)的庫(kù),可能會(huì)帶來(lái)一些好處……因?yàn)檫@樣你可以把它分離出來(lái)……我覺(jué)得結(jié)構(gòu)體標(biāo)簽是反射中最安全的部分,你導(dǎo)入 reflect 包后,其他部分可能不一定更糟,但絕對(duì)有些嚇人……所以有一個(gè)邊界,你可以只處理結(jié)構(gòu)體標(biāo)簽---把它分離出來(lái),可能會(huì)有所幫助,比如“好吧,我這里只是在看結(jié)構(gòu)體標(biāo)簽。”
Mat Ryer: 是的,我懂你的意思,這樣你就不必把整個(gè) reflect 包導(dǎo)入到代碼中。而且我認(rèn)為 reflect 包中也包含了 unsafe,盡管很多非常普通的包也確實(shí)有 unsafe……但我懂你的意思……你可以只導(dǎo)入一個(gè)解析結(jié)構(gòu)體標(biāo)簽的包,比如 reflect/struct tags 之類的。我還挺喜歡這個(gè)想法的。你應(yīng)該告訴別人這個(gè)想法。
Jaana Dogan: 我記得 Go 早期的時(shí)候,他們說(shuō)“嘿,如果你導(dǎo)入了 reflect 包,那可不妙。” 那時(shí)候幾乎認(rèn)為這是不安全的,因?yàn)槟阋惨蕾囉?unsafe 出于很多其他原因……但你知道,那是你不應(yīng)該看到的導(dǎo)入行之一,或者你應(yīng)該非常謹(jǐn)慎;如果你使用它,你應(yīng)該非常小心地控制它的用法,等等。但你知道,突然之間,大家開(kāi)始導(dǎo)入 reflect,因?yàn)樗隽撕芏嗷A(chǔ)性工作,比如結(jié)構(gòu)體標(biāo)簽……所以我覺(jué)得如果它是一個(gè)單獨(dú)的包,用戶的心理上可能會(huì)有更多的分離感。這樣你可以編寫 linter 工具來(lái)捕捉 reflect 的導(dǎo)入……但一些基礎(chǔ)或更簡(jiǎn)單的擔(dān)憂可以存在于不同的包中。
我見(jiàn)過(guò)的一些與此相關(guān)的做法是,如果人們想依賴 reflect 包,他們不會(huì)到處導(dǎo)入它;他們只是把 reflect 的所有用法封裝在一個(gè)單獨(dú)的包中,然后從那個(gè)包中提供一些工具。你見(jiàn)過(guò)這樣的做法嗎,或者你做過(guò)類似的事情嗎?
Mat Ryer: 沒(méi)有,但這對(duì)我來(lái)說(shuō)很有道理。至少這樣你就把所有的怪異集中在一個(gè)地方……但我不確定這是不是一種健康的做法,因?yàn)檫@有點(diǎn)像“廚房水槽”或“工具包”那種……
Jon Calhoun: 我確實(shí)做過(guò)將所有 reflect 相關(guān)的代碼放在一個(gè)源文件中的做法,但我從來(lái)沒(méi)有在 Go 的反射中做過(guò)足夠大的東西,需要走到那一步……不過(guò),我確實(shí)可以說(shuō),我在 Ruby 中確實(shí)瘋狂使用過(guò)一些元編程的東西,但當(dāng)我轉(zhuǎn)到 Go 時(shí),我并不覺(jué)得那是寫 Go 代碼的正確方式,所以我盡可能避免它。
Mat Ryer: 是的。我見(jiàn)過(guò)一個(gè)例子,有人為了做一個(gè)好公民,他們打算把一些數(shù)據(jù)放到 map 里,但如果 map 是 nil 的話,程序就會(huì) panic……所以他們實(shí)際上使用了(我認(rèn)為是)JSON 解組器;如果 map 是 nil,它會(huì)直接解組---他們?cè)诖a中直接寫了兩個(gè)小花括號(hào),表示一個(gè)空對(duì)象。然后它使用這種技術(shù)創(chuàng)建了一個(gè) map……這意味著作為程序員,你可以傳入一個(gè) nil map,它仍然能工作。不過(guò),這有點(diǎn)太“魔法”了,而且有時(shí)讓它 panic 也沒(méi)什么問(wèn)題,或者……因?yàn)樗且粋€(gè)庫(kù),有時(shí)我不介意捕獲那些會(huì)導(dǎo)致 panic 的情況,然后用一個(gè)更好的錯(cuò)誤信息來(lái) panic,比如“你必須在傳入之前創(chuàng)建 map”之類的。
但我確實(shí)見(jiàn)過(guò)一些不必要使用 reflect 的情況,但人們?yōu)榱擞脩魢L試多做了一些。這些情況挺有趣的。
另一種反射的形式是 Go 中的 AST 包,和一些實(shí)際的代碼反射、代碼分析包……它們也在不斷改進(jìn)。剛開(kāi)始的時(shí)候,它們非常難用,現(xiàn)在有一些更高層次的包讓它變得更容易了。我們有一個(gè)項(xiàng)目,我們實(shí)際上用 Go 接口描述了我們的 API,我們使用了 AST 包---有一個(gè) packages 包可以讓你打開(kāi)一個(gè)包,然后你可以遍歷接口,檢查接口中的字段之類的東西……所以它做了那種反射;它以自己的結(jié)構(gòu)表示數(shù)據(jù),然后使用這些數(shù)據(jù)從模板生成代碼。
所以這很棒,因?yàn)槲覀兯械?API 都是用 Go 接口描述的,作為 Go 開(kāi)發(fā)者,這對(duì)我們來(lái)說(shuō)非常容易理解和推理……而且它是真正的 Go 包,因此也是類型安全的。你不能使用無(wú)效的類型,所以這是描述 API 的一種很棒的方式。你知道它會(huì)工作。我們可以從中生成客戶端,生成服務(wù)器端代碼,以及處理所有那個(gè)樣板代碼的 HTTP 邏輯……任何樣板代碼都可以生成,我們甚至生成了另一個(gè)接口,實(shí)際上和原來(lái)的接口略有不同,因?yàn)樗邮芤粋€(gè)上下文,并返回一個(gè)錯(cuò)誤,而我們?cè)诙x時(shí)省略了這些。
所以我們可以寫下我們的定義接口,運(yùn)行代碼生成器,然后實(shí)現(xiàn)接口,僅此而已。我們就有了一個(gè)新的服務(wù),然后可以在我們的項(xiàng)目中公開(kāi)了。
Jaana Dogan: 你們什么時(shí)候開(kāi)源這個(gè)項(xiàng)目?
Jon Calhoun: 我想他已經(jīng)開(kāi)源了。
Mat Ryer: 是的,已經(jīng)開(kāi)源了,叫 Oto[5]。
Jaana Dogan: 真的嗎?!
Mat Ryer: 是的,叫 Oto。
Jaana Dogan: 很棒。
Mat Ryer: 目前它基本上是一個(gè) JSON API,但實(shí)際上,因?yàn)樗皇谴a生成,模板也可以修改,所以你可以很容易地為它編寫一個(gè)二進(jìn)制協(xié)議,或者其他任何類型的協(xié)議。是的,挺不錯(cuò)的。有人為它寫了一個(gè) Rust 服務(wù)器模板……這有點(diǎn)怪,但也挺酷的。我們會(huì)把鏈接放在節(jié)目筆記里。它的地址是 github.com/pacedotdev/oto[6],我會(huì)把它放在節(jié)目筆記里,供有興趣的人參考。我們?cè)谏a(chǎn)環(huán)境中使用它,效果非常好。我是說(shuō),我們的用例相對(duì)簡(jiǎn)單,但它真的很好用……這其實(shí)也是一種反射,因?yàn)槲覀儽仨氁跃幊谭绞綑z查那些接口,然后對(duì)它們進(jìn)行操作。
Jon Calhoun: 所以 Mat,我猜你是用 Oto 來(lái)生成代碼的,對(duì)吧?
Mat Ryer: 是的,基本上就是這樣。它接收 Go 接口,把它們和模板混合,然后生成新的代碼。
Jon Calhoun: 我想說(shuō)的是,我們經(jīng)常說(shuō)反射不好,或者你應(yīng)該盡量避免它,因?yàn)樗茈y理解,也很難維護(hù)……但我覺(jué)得有時(shí)候這很難,因?yàn)槲覀儧](méi)有告訴人們替代的方法……而我認(rèn)為代碼生成是一個(gè)非常有用的替代方案。就像你說(shuō)的,你其實(shí)是在做反射的事情,分析代碼,然后生成代碼,最終得到的東西更容易管理。
我甚至見(jiàn)過(guò)一些 ORM 采用這種方法;我記得 SQLBoiler[7] 就是其中之一,它會(huì)掃描你的 SQL 數(shù)據(jù)庫(kù),然后從中生成 Go 結(jié)構(gòu)體……所以它不是使用反射,而是直接生成與你的數(shù)據(jù)庫(kù)完全匹配的東西,你可以直接使用它們……這是一種完全不同的做法,但我認(rèn)為限制反射的使用讓人們?nèi)タ紤]其他方法,并決定“這是更好的選擇嗎?這是更容易維護(hù)的嗎?”
Mat Ryer: 是的。還有 go generate 命令;你可以在代碼中加一個(gè)注釋,一個(gè)特殊的注釋……這有點(diǎn)像魔法,但你可以寫 //go:generate,然后加上一條命令。如果你在項(xiàng)目中輸入這個(gè)命令,它就會(huì)執(zhí)行這些命令。這對(duì)于那種需要在構(gòu)建前生成代碼的情況非常有用……這是一個(gè)不錯(cuò)的做法,因?yàn)槟憧梢垣@得類型安全,編譯器會(huì)幫助你;也許一開(kāi)始沒(méi)有,但一旦代碼生成出來(lái),它通常就是你項(xiàng)目的一部分了,接著就可以構(gòu)建了……如果它有問(wèn)題,你很快就會(huì)發(fā)現(xiàn)。
Jaana Dogan: 是的,這正是我想說(shuō)的……我認(rèn)為 ast 包和 reflect 包的區(qū)別在于,ast 包是一種美學(xué)上的選擇;它并不是在運(yùn)行時(shí)執(zhí)行的。所以你生成代碼后,仍然具有類似的可維護(hù)性和類型安全性。你只是用編譯器生成了一些代碼。如果你能將一些問(wèn)題交給代碼生成來(lái)解決,那絕對(duì)是值得做的。
Mat Ryer: 是的,這個(gè)觀點(diǎn)很好。
Jon Calhoun: 我們之前討論過(guò)泛型問(wèn)題,其實(shí)我很想看到一種泛型的實(shí)現(xiàn),基本上在一開(kāi)始就運(yùn)行 go generate。你寫的代碼就像泛型已經(jīng)存在一樣,按照某個(gè)提案去寫,然后它會(huì)在某個(gè)預(yù)處理步驟中將其編譯成 Go 代碼,生成需要的內(nèi)容,然后繼續(xù)處理……我認(rèn)為這是可行的,雖然需要一些復(fù)雜的工作。
Mat Ryer: 我和我的一個(gè)朋友寫了一個(gè)類似的項(xiàng)目,叫 Jenny。有人在用它。它使用一種特殊的類型,基本上就是一個(gè)接口類型,放在一個(gè)不同的包里……我想這還是用了 AST 的技術(shù);它會(huì)找到這些實(shí)例,并查找出你在命令中列出的類型。你運(yùn)行一個(gè)命令,列出你想支持的類型,然后它就是一個(gè)復(fù)制粘貼的過(guò)程,替換掉代碼中提到的那些類型。它不是完美的,因?yàn)槟悴荒軐?duì)它進(jìn)行類型斷言;一旦那樣做,它就很奇怪了……但在簡(jiǎn)單的情況下,它是有效的。我想這就是你說(shuō)的那種東西。
Jon Calhoun: 我用過(guò)類似的東西……我在想的是,可能可以把這個(gè)想法拓展得更遠(yuǎn),像現(xiàn)在的泛型提案那樣。讓你可以完全按照泛型的方式寫代碼。因?yàn)榉盒偷囊粋€(gè)問(wèn)題是,它讓編譯和其他步驟變得更加復(fù)雜。所以,與其把它直接集成到編譯器中,不如在預(yù)編譯步驟中處理它,使其看起來(lái)像是已經(jīng)內(nèi)置在語(yǔ)言中,但實(shí)際上并不是,這樣在那個(gè)步驟進(jìn)行轉(zhuǎn)換……
Mat Ryer: 對(duì)。
Jon Calhoun: 當(dāng)然,這可能會(huì)變得非常麻煩,以至于不值得去做……
Jaana Dogan: 對(duì)我來(lái)說(shuō),泛型一直像是“嘿,這里有個(gè)模板,你用它生成一些東西,編譯器處理所有這些事情,因?yàn)樯傻拇a復(fù)雜得讓人難以理解。” 這就是為什么語(yǔ)言需要提供一些語(yǔ)法糖,讓你能夠與這些類型進(jìn)行交互。所以我想,如果你暴露出生成的內(nèi)容,用戶會(huì)覺(jué)得非常可怕。你會(huì)有各種不同的---類型,和各種不同的情況,等等……所以我覺(jué)得對(duì)于很多情況來(lái)說(shuō),這看起來(lái)不會(huì)很好。這可能會(huì)讓人們一開(kāi)始就不愿意使用泛型。
這就是為什么我在等待真正的泛型提案和實(shí)現(xiàn),我想看看那個(gè)語(yǔ)法糖到底是什么樣子……即使實(shí)際的難題對(duì)我來(lái)說(shuō)是不可見(jiàn)的,至少---我對(duì)底層生成的東西其實(shí)不感興趣,因?yàn)槲抑浪诤芏嗲闆r下會(huì)非常復(fù)雜。
我認(rèn)為這些代碼生成器沒(méi)有真正流行起來(lái)的原因之一是,你需要一種官方認(rèn)可的泛型解決方案。作為一個(gè)庫(kù),我不能隨便選擇一個(gè)工具,而不是另一個(gè)。實(shí)際上沒(méi)有太多的實(shí)驗(yàn)。你不能真的暴露底層的東西;我只想要一個(gè)對(duì)所有人都有效的東西,這樣我們可以達(dá)成共識(shí),所有的庫(kù)系統(tǒng)都能切換到它……我不太關(guān)心底層生成的是什么,它們可以隨時(shí)優(yōu)化,或者做其他事情……在這個(gè)領(lǐng)域已經(jīng)有很多工作被做了,所以我們不是在第一次嘗試解決這個(gè)問(wèn)題。
我認(rèn)為我們應(yīng)該找到泛型的解決方案。它應(yīng)該是官方語(yǔ)言的一部分。我覺(jué)得不需要太多的實(shí)驗(yàn)……但這對(duì)人們來(lái)說(shuō)會(huì)很難,因?yàn)樗隙〞?huì)讓語(yǔ)言變得更加復(fù)雜。
Mat Ryer: 是的,但是你知道,很多 JavaScript 庫(kù)采用了一種方法,就是使用一個(gè)墊片(shim)……我記得最初的 TypeScript 或者 Google 的 Dart 最初只是編譯成 JavaScript,雖然看起來(lái)很丑,但 Jaana,你只需要不要看它。只要把最后的文件命名為“別看這個(gè).go”之類的。
Jaana Dogan: [笑]
Jon Calhoun: 我覺(jué)得你必須訓(xùn)練人們……這有點(diǎn)像你在編譯器之上又構(gòu)建了一個(gè)編譯器,而它才是給你報(bào)錯(cuò)、與你互動(dòng)的那個(gè)……然后無(wú)論它最終編譯成什么,你都得把它藏在某個(gè)捆綁文件夾里之類的……
Mat Ryer: 是的,但你想想 IDE 和所有工具……一旦“語(yǔ)法無(wú)效”,所有工具就都?jí)牧恕?/p>
Jon Calhoun: 現(xiàn)在會(huì)更難。但隨著他們對(duì)不同 IDE 如何使用語(yǔ)言服務(wù)器的改進(jìn),希望這種實(shí)驗(yàn)……
Jaana Dogan: Gopls,是的。
Jon Calhoun: 是的。基本上,因?yàn)樗鼈兌荚谑褂靡粋€(gè)通用的……我忘記叫什么了,但基本上就是語(yǔ)言服務(wù)器---有一個(gè)通用的規(guī)范,所有語(yǔ)言都可以實(shí)現(xiàn)……所以希望這種工作能帶來(lái)更多在現(xiàn)有語(yǔ)言上進(jìn)行實(shí)驗(yàn)的可能性……這會(huì)很有趣。
Mat Ryer: 你能簡(jiǎn)要介紹一下什么是語(yǔ)言服務(wù)器嗎?我記得它叫 LSP,對(duì)吧?語(yǔ)言服務(wù)器協(xié)議。
Jon Calhoun: 聽(tīng)起來(lái)是對(duì)的。一般的想法是,與其讓每個(gè) IDE 或編輯器都自己實(shí)現(xiàn) Go 的自動(dòng)補(bǔ)全和 JavaScript 的自動(dòng)補(bǔ)全,不如標(biāo)準(zhǔn)化它。我記得 VS Code 是第一個(gè)這么做的,但現(xiàn)在其他編輯器也在用了。
Jaana Dogan: 是的,我覺(jué)得它來(lái)自微軟,我不太確定……
Mat Ryer: 是的,確實(shí)來(lái)自微軟的 Visual Studio Code。
Jon Calhoun: 這個(gè)想法是,他們提出了一個(gè)類似于 Go 中接口的東西,或者說(shuō)“這是一個(gè) LSP 應(yīng)該為某種語(yǔ)言提供的東西。” 基本上,它應(yīng)該有一些你需要實(shí)現(xiàn)的方法,它可以根據(jù)用戶所在的位置給出自動(dòng)補(bǔ)全建議……想法是你可以為任何語(yǔ)言實(shí)現(xiàn)這個(gè),然后任何 IDE 或編輯器都可以利用它來(lái)實(shí)現(xiàn)編輯器中的自動(dòng)補(bǔ)全。
Mat Ryer: 是的,這太棒了……老實(shí)說(shuō),我真的無(wú)法相信它能工作,因?yàn)樗姓Z(yǔ)言差異這么大。我們是怎么找到一個(gè)協(xié)議,能夠描述所有這些的?我覺(jué)得這真的挺神奇的,毫無(wú)疑問(wèn),這個(gè)協(xié)議肯定不簡(jiǎn)單。
Jon Calhoun: 這大概是那種1%的極端情況處理得不太好,但對(duì)大多數(shù)開(kāi)發(fā)者來(lái)說(shuō),這并不重要,LSP 的好處遠(yuǎn)遠(yuǎn)超過(guò)了這些問(wèn)題。
Mat Ryer: 是的。
Jon Calhoun: 但仍然存在一個(gè)問(wèn)題---這不一定是問(wèn)題,不同的編輯器會(huì)用不同的方法來(lái)實(shí)現(xiàn)這個(gè)。我記得 GoLand 是其中一個(gè)不使用語(yǔ)言服務(wù)器的編輯器;他們完全使用內(nèi)部的實(shí)現(xiàn)。在某些方面,這有好處……因?yàn)?Gopls 剛出來(lái)時(shí),確實(shí)很脆弱。但我記得他們的方案在當(dāng)時(shí)對(duì) Go modules 的支持要更好,不過(guò)現(xiàn)在我不確定是否還是這樣。
Mat Ryer: 我聽(tīng)說(shuō)過(guò)很多好評(píng)。
Jaana Dogan: JetBrains 通常就是這么做的。他們一切都自己做,這是他們的特色。
Mat Ryer: 是的。我唯一的感受是,每次我不得不使用 Java 時(shí),我都接觸過(guò) Eclipse IDE,真的……這是一種美學(xué)上的感受。我用 Visual Studio Code 是因?yàn)樗雌饋?lái)更好看,你花那么多時(shí)間在里面……我覺(jué)得這確實(shí)很重要。我覺(jué)得你想要一個(gè)美好的使用體驗(yàn)。但我聽(tīng)說(shuō) GoLand 編輯器有一些很棒的功能,能做很多事情。我還沒(méi)試過(guò),但……嗯,我會(huì)感興趣。如果有人想給我發(fā)推特,告訴我他們的體驗(yàn),我可能會(huì)讀一讀。
Jon Calhoun: 可能會(huì)。
Mat Ryer: 你猜現(xiàn)在是什么時(shí)間……?[笑]
Jon Calhoun: 我想我們都知道,可能是“非主流觀點(diǎn)時(shí)間”。
Mat Ryer: 是的,非主流觀點(diǎn)時(shí)間!
Mat Ryer: 那么,有沒(méi)有人有不太受歡迎的觀點(diǎn)呢?我們已經(jīng)說(shuō)過(guò)的一些事情可能有點(diǎn)不太受歡迎,但……你們有什么特別想分享的嗎?
Jaana Dogan: 我有一個(gè)……
Jon Calhoun: 繼續(xù)吧,Jaana。我讓她來(lái)主導(dǎo)這部分。
Jaana Dogan: 我覺(jué)得我們需要泛型。我知道這可能不是一個(gè)非常不受歡迎的觀點(diǎn),但……我從這門語(yǔ)言一開(kāi)始就這么說(shuō)了,結(jié)果大家都討厭我……但我覺(jué)得我們確實(shí)需要泛型。
Jon Calhoun: 我完全同意,因?yàn)槲易鲞^(guò)足夠多的工作---我覺(jué)得一個(gè)例子是,如果 Go 想在教育領(lǐng)域表現(xiàn)得好,比如讓人們?cè)诖髮W(xué)里學(xué)習(xí)它,他們會(huì)接觸到數(shù)據(jù)結(jié)構(gòu),而沒(méi)有泛型很難處理數(shù)據(jù)結(jié)構(gòu)……我認(rèn)為這是 Java 在學(xué)校里被廣泛教授的原因之一,因?yàn)樗谶@方面做得很好。
Mat Ryer: 是的。你怎么看最近的泛型提案?
Jon Calhoun: 我看過(guò)的最近一個(gè)提案我挺喜歡的---我還沒(méi)深入研究過(guò),但對(duì)我來(lái)說(shuō)看起來(lái)沒(méi)問(wèn)題……我對(duì)細(xì)節(jié)要求不高,我的需求相對(duì)簡(jiǎn)單。
Mat Ryer: 我覺(jué)得他們?cè)谠O(shè)計(jì)上取得了很大進(jìn)展……當(dāng)然,你經(jīng)常會(huì)聽(tīng)到大家真正關(guān)心的是如何實(shí)現(xiàn)它,以及這對(duì) Go 的類型系統(tǒng)會(huì)有什么影響,維護(hù)起來(lái)會(huì)有多難,等等……聽(tīng)到 Go 團(tuán)隊(duì)和其他貢獻(xiàn)者優(yōu)先考慮這些問(wèn)題真是太好了,因?yàn)檫@確實(shí)至關(guān)重要。我非常不希望看到 Go 變得太復(fù)雜,以至于我們?cè)僖膊荒芴砑有鹿δ芰恕栽谶@一點(diǎn)上,我和你站在同一陣線,Jaana。
Jaana Dogan: 其實(shí),我已經(jīng)對(duì)這個(gè)話題感到非常疲憊了,一年前就不再關(guān)注這些提案了。
Mat Ryer: 哇。你是因?yàn)榍榫w波動(dòng)太大嗎?
Jaana Dogan: 并不是情緒波動(dòng)大,而是我對(duì)每個(gè)提案都有至少 50 個(gè)顧慮……
Mat Ryer: 哦,才 50 個(gè)顧慮……
Jaana Dogan: 而且沒(méi)有簡(jiǎn)單的答案……是的,我是說(shuō),有些是宏觀層面的……
Mat Ryer: [笑] 你讓其他人情緒波動(dòng)了。
Jaana Dogan: 沒(méi)錯(cuò)。我覺(jué)得我并沒(méi)有真正為討論做出貢獻(xiàn)……而且我心里所擔(dān)憂的那些點(diǎn),你無(wú)法真正預(yù)見(jiàn)實(shí)際情況會(huì)是什么樣子,因?yàn)檫@真的取決于那些會(huì)使用泛型的人……隨著時(shí)間的推移,我們會(huì)看到它對(duì)整個(gè)庫(kù)生態(tài)系統(tǒng)的實(shí)際影響。所以我當(dāng)時(shí)覺(jué)得,“嘿,我并沒(méi)有真正為這次討論貢獻(xiàn)什么。” 我很興奮它正在發(fā)生。那些人---我?guī)缀蹩梢钥隙ㄋ麄兎浅T谝狻K麄儽任腋谝猓晕矣X(jué)得沒(méi)有必要再試圖參與了。
Jon Calhoun: 這讓我想起了類型別名,以及它當(dāng)時(shí)受到了多少抵制,大家都說(shuō)它會(huì)毀掉這門語(yǔ)言……但自從它被引入后,我?guī)缀鯖](méi)有看到它被廣泛使用---偶爾你會(huì)看到它,我也做過(guò)一些奇怪的事情,只是為了看看能做些什么……但我覺(jué)得我沒(méi)遇到過(guò)濫用它的庫(kù),這有點(diǎn)有趣,因?yàn)槟阒翱吹降膹?qiáng)烈反對(duì)意見(jiàn)……我理解他們的顧慮,我并不是說(shuō)人們不應(yīng)該表達(dá)顧慮,只是有趣的是,這些擔(dān)憂根本沒(méi)有成為現(xiàn)實(shí)。
Mat Ryer: 是的,但它確實(shí)被引入了。
Jaana Dogan: 我剛好想舉這個(gè)作為例子,因?yàn)槟鞘俏业谝淮胃械綄?duì)這個(gè)項(xiàng)目感到倦怠的時(shí)刻,我當(dāng)時(shí)想去做其他事情……一切都變成了關(guān)于各種可能性的無(wú)盡討論……所以對(duì)于泛型---我不想再參與,因?yàn)橐呀?jīng)有太多聲音了,而這是一項(xiàng)艱難的工作,你無(wú)法預(yù)見(jiàn)未來(lái)。
我也信任 Go 開(kāi)發(fā)者,因?yàn)楹芏嗳朔浅W⒅睾?jiǎn)單性,所以他們不會(huì)濫用某個(gè)特性。我認(rèn)為 Go 的用戶對(duì)于自己想使用的語(yǔ)言核心子集非常了解……這門語(yǔ)言并不大,但我也信任更大的生態(tài)系統(tǒng)。所以我現(xiàn)在不再那么擔(dān)心了。
Mat Ryer: 是的,我的意思是,你總是可以選擇不用它……坦白說(shuō),我直到很晚才意識(shí)到---類型別名已經(jīng)進(jìn)入了這門語(yǔ)言。我記得當(dāng)時(shí)的提案和大討論,但……另一個(gè)提案是關(guān)于錯(cuò)誤處理的 Try 提案;對(duì)我來(lái)說(shuō),我對(duì)它過(guò)敏,因?yàn)樗杏X(jué)太像“魔法”了……而且我覺(jué)得它不符合 Go 的哲學(xué)。我們必須小心,不要因?yàn)橄矚g Go 現(xiàn)在的樣子而過(guò)于僵化,不允許任何演變,但……我覺(jué)得你是對(duì)的,Jaana---我們確實(shí)意識(shí)到簡(jiǎn)單性和反對(duì)“魔法”的重要性。我們作為一個(gè)社區(qū),對(duì)此非常清楚。而且,是的,我想你總是可以選擇不使用它,如果你不喜歡的話……
Jon Calhoun: 我覺(jué)得這可能是為什么反射是一個(gè)如此奇怪的話題的原因,因?yàn)槿藗儊?lái)自其他語(yǔ)言,在那些語(yǔ)言中使用反射是完全正常的。我想我在我們開(kāi)始錄音之前已經(jīng)說(shuō)過(guò)了,但我不認(rèn)為如果沒(méi)有反射和元編程,Ruby 會(huì)如此受歡迎。Rails 和你能用它做的所有瘋狂的事情,都是元編程的副產(chǎn)品,而在很多方面,它讓那門語(yǔ)言更具生產(chǎn)力……但所有這些在 Go 中完全沒(méi)有意義。
所以當(dāng)人們從那樣的語(yǔ)言轉(zhuǎn)到 Go 時(shí),他們會(huì)想,“為什么這個(gè) reflect 庫(kù)這么難用?為什么每個(gè)人都告訴我不要用它?”,這是一個(gè)很難的心理轉(zhuǎn)變,我覺(jué)得這是因?yàn)榻鉀Q問(wèn)題的方法不同,優(yōu)先級(jí)也不同。
Mat Ryer: 是的,我覺(jué)得你說(shuō)得對(duì)。有時(shí)我會(huì)---與其維護(hù)……比如有一段代碼用了反射,與其維護(hù)它,我會(huì)重寫它,因?yàn)閷?duì)我來(lái)說(shuō),重寫的過(guò)程是我弄清楚發(fā)生了什么的方式。這就是問(wèn)題所在---對(duì)我來(lái)說(shuō),維護(hù)它其實(shí)很難,我會(huì)放棄,轉(zhuǎn)而選擇重寫它……坦白說(shuō),我如果可以的話,通常都會(huì)這么做,因?yàn)槲铱偸前l(fā)現(xiàn)重寫是獲得更好版本的方式……你在重寫過(guò)程中學(xué)到了很多,第二次寫的代碼總是好得多。但我不知道,這確實(shí)是一個(gè)有趣的問(wèn)題。
好吧,我想今天的時(shí)間就到這里了……下周我們有一個(gè)非常有趣的節(jié)目。我們會(huì)邀請(qǐng)一個(gè)剛剛得到 Go 開(kāi)發(fā)工作的人,還會(huì)邀請(qǐng)一個(gè)正在學(xué)習(xí) Go、還在高中里的學(xué)生。我們會(huì)看看人們是如何進(jìn)入我們稱之為編程的這個(gè)瘋狂世界的。
Jon,非常感謝你。Jaana,總是很愉快。我們下次見(jiàn)!
Mat Ryer: 就這樣。
Jon Calhoun: 我本來(lái)想彈吉他的……
Mat Ryer: 我得做個(gè)空氣吉他,對(duì)吧……
Jon Calhoun: 你背景里有吉他,但你卻說(shuō):“不彈……”
Mat Ryer: 是的,但我不會(huì)在 [聽(tīng)不清 01:01:02.08] 上彈。Jaana,你曾同意過(guò)我看起來(lái)像個(gè)魔術(shù)師。你還記得嗎?
Jaana Dogan: 是的,你看起來(lái)像個(gè)魔術(shù)師。
Mat Ryer: 是的。你知道這有多難嗎?你覺(jué)得理所當(dāng)然,但我不得不跟我父母坦白。我說(shuō):“媽媽,爸爸,坐下。選一張牌吧。”
Jaana Dogan: [笑] 但你知道嗎,這種胡子造型,這種風(fēng)格……非常魔術(shù)師的感覺(jué)。
Mat Ryer: 確實(shí)很像魔術(shù)師的。太荒謬了。我應(yīng)該換個(gè)造型,但……
Jaana Dogan: [笑]
Jon Calhoun: 我可以想象……你對(duì)父母坦白自己是個(gè)魔術(shù)師,他們說(shuō):“我們得給他買臺(tái)電腦,或者別的什么……讓他去編程吧。”
Mat Ryer: 是的,讓他成為一個(gè) Go 程序員,因?yàn)檫@里沒(méi)有魔法。我不知道一個(gè)家庭為什么會(huì)反對(duì)魔法,但……也許有一些隱藏的背景故事。
Jon Calhoun: 也許你父母是覺(jué)得:“他得搬出去住了。這行不掙錢。”
Mat Ryer: [笑]
Jon Calhoun: 我其實(shí)不知道魔術(shù)師掙多少錢,但我猜應(yīng)該很難入行……
Mat Ryer: 是的,我覺(jué)得很困難……嗯,我想不出笑話……真可惜,因?yàn)檫@里肯定有很多笑話等著讓我從空氣中抓出來(lái)。嗯,差不多夠了……我喜歡講完笑話后的尷尬沉默,那是我最喜歡的部分。
參考資料
[1]
Mat: https://github.com/matryer
[2]Jon: https://github.com/joncalhoun
[3]Jaana: https://github.com/rakyll
[4]#133 Reflection and meta programming: https://changelog.com/gotime/133
[5]Oto: https://github.com/pacedotdev/oto
[6]github.com/pacedotdev/oto: https://github.com/pacedotdev/oto
[7]SQLBoiler: https://github.com/volatiletech/sqlboiler





































