<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 在 Google:服務(wù)于軟件工程的語言設(shè)計(翻譯)(二)

          共 8826字,需瀏覽 18分鐘

           ·

          2020-09-24 13:02

          原文:Go at Google: Language Design in the Service of Software Engineering

          地址:https://talks.golang.org/2012/splash.article

          作者:Rob Pike

          翻譯:Jayce Chant(博客:jaycechant.info,公眾號ID:jayceio)


          Rob Pike:Unix 小組成員,參與了 Plan 9 計劃,1992 年和 Ken Thompson 共同開發(fā)了 UTF-8。他和 Ken Thompson 也是 Go 語言最早期的設(shè)計者。

          譯文較長,分三篇推送,這里是第 8~13 小節(jié)。

          第一篇(1~7 小節(jié)):Go 在 Google:服務(wù)于軟件工程的語言設(shè)計(翻譯)(一)

          8. 包

          Go 的包系統(tǒng)設(shè)計,將庫、命名空間、模塊的一些特性結(jié)合在一起,變成一個統(tǒng)一的結(jié)構(gòu)。

          譯者注:2012 年 Go 1.0 ,包管理使用的還是最簡單的 GOPATH 模式。之后這種基于 Google 單一代碼庫的設(shè)計造成了各種不便,第三方包管理工具百花齊放。2015 年 Go 1.5 引入 Vendor 機制,到后面發(fā)現(xiàn)還是沒有解決問題。第三方工具 dep 一度最有希望轉(zhuǎn)正,結(jié)果 2018 年官方推出 vgo (后改名 Go Modules 并入 go 工具鏈)統(tǒng)一了機制,到 2020 年的 1.14 正式宣布 Go Modules "ready for production"。

          跟 20102 相比,現(xiàn)在 Go 的包管理已經(jīng)有了很多變化,最主要的是引入了 module 的概念。

          每一個 Go 源文件,例如 "encoding/json/json.go",都會以一個 package 語句開始,像這樣:

          package?json

          其中 json 是 『包名』,一個簡單的標(biāo)識符。包名通常是簡明扼要的。

          要使用一個包,導(dǎo)入語句里的包路徑標(biāo)識了要導(dǎo)入的文件?!郝窂健坏暮x并未在語言中指定,但在實踐中,按照慣例,它是源包在代碼庫里的目錄路徑,以斜杠 / 分隔,像:

          import?"encoding/json"

          然后,在導(dǎo)入的源文件(importing,調(diào)用方)里引用時,用包名(區(qū)別于路徑)來修飾(qualify)被導(dǎo)入(imported)包的包中成員:

          var?dec?=?json.NewDecoder(reader)

          這種設(shè)計清晰明確。Name 對比 pkg.Name ,人們總是可以從語法中判斷出一個名字是否來自本地包。(這一點后面會有更多的介紹。)

          在我們的例子中,包的路徑是 "encoding/json",而包名是 json。在標(biāo)準(zhǔn)倉庫之外,慣例是將項目或公司名稱放在命名空間的根部:

          import?"google/base/go/log"

          重要的是要認(rèn)識到包的路徑是唯一的,但對包名卻沒有這樣的要求。路徑必須唯一地標(biāo)識要導(dǎo)入的包,而包名只是一個約定,讓包的調(diào)用方可以引用它的內(nèi)容。包名不需要是唯一的,可以在每個導(dǎo)入(importing)的源文件里,通過在導(dǎo)入語句中提供一個本地標(biāo)識符來重命名。下面兩個導(dǎo)入都引用了包名為 log 的包,但要在同一個源文件里導(dǎo)入它們,必須(在本地)重命名其中一個包。

          import?"log"?//?標(biāo)準(zhǔn)包
          import?googlelog?"google/base/go/log"?//?Google專用包

          每個公司可能都有自己的 log 包,沒有必要讓包名獨一無二。恰恰相反:Go 的風(fēng)格建議保持包名短小精悍、清晰明確,而不是擔(dān)心重名 。

          還有一個例子:在 Google 的代碼庫里,有很多個 server 包。

          9. 遠(yuǎn)程包

          Go 包系統(tǒng)的一個重要特性是,包的路徑一般可以是任意字符串,可以用它標(biāo)識托管代碼倉庫的站點 URL ,以此來引用遠(yuǎn)程代碼庫。

          下面是使用 github 上的 doozer 包的方法。go get 命令使用 go 構(gòu)建工具從站點獲取倉庫并安裝它。一旦安裝完畢,它就可以像其他普通的包一樣被導(dǎo)入和使用。

          $?go?get?github.com/4ad/doozer?//?獲取包的?Shell?命令
          import?"github.com/4ad/doozer"?//?Doozer?調(diào)用方的?import?語句

          var?client?doozer.Conn?????????//?調(diào)用方對包的引用

          值得注意的是,go get 命令以遞歸的方式下載依賴,正是因為依賴關(guān)系是顯式的所以才可以這樣實現(xiàn)。另外,區(qū)別于其它語言使用的集中式包注冊,Go 導(dǎo)入路徑的命名空間分配依賴于 URL,這使得包的命名是去中心化的,因而是可擴展的。

          10. 語法

          語法就是一門編程語言的用戶界面。**盡管語法對語義影響有限,而語義很可能才是語言更重要的組成部分,但語法決定了語言的可讀性,繼而決定了語言的清晰度。**同時,語法對工具鏈而言至關(guān)重要:如果一門語言難以解析,就很難為其編寫自動化工具。

          因此,Go 在設(shè)計時就考慮了語言的清晰度和工具鏈,并且擁有簡潔的語法。與 C 家族的其他語言相比,它的語法規(guī)模不大,只有 25 個關(guān)鍵字(C99 有 37 個;C++11 有 84 個;而且這兩個數(shù)字還在繼續(xù)增加)。更重要的是,語法很規(guī)范,所以很容易解析(應(yīng)該說大多數(shù)規(guī)范;也有個別怪異的語法我們本可以改善結(jié)果發(fā)現(xiàn)得太晚)。與 C 和 Java,尤其是 C++ 不同,Go 可以在沒有類型信息或符號表的情況下進行解析;不需要類型相關(guān)的上下文。語法容易推導(dǎo),工具自然就容易編寫。

          Go 語法里有一個細(xì)節(jié)會讓 C 程序員感到驚訝,那就是聲明語法更接近 Pascal 而不是 C。聲明的名稱出現(xiàn)在類型之前,并且使用了更多關(guān)鍵字(譯者注:指 vartype關(guān)鍵字):

          var?fn?func([]int)?int
          type?T?struct
          ?{?a,?b?int?}

          對比 C 語言

          int?(*fn)(int[]);
          struct?T?{?int?a,?b;?}

          無論對人還是對計算機來說,由關(guān)鍵字引入的聲明都更容易解析,而且使用 類型語法 而不是 C 那樣的 表達(dá)式語法 ,對解析有很大的幫助:它增加了語法,但消除了歧義。你還有另外一個選擇:對于初始化聲明,可以丟棄 var 關(guān)鍵字,直接從表達(dá)式中推斷變量的類型。這兩個聲明是等價的;第二個聲明更短也更地道:

          var?buf?*bytes.Buffer?=?bytes.NewBuffer(x)?//?顯式指定類型
          buf?:=?bytes.NewBuffer(x)??????????????????//?類型推斷

          在 golang.org/s/decl-syntax 有一篇博客文章,詳細(xì)介紹了 Go 的聲明語法,以及為什么它與 C 語言如此不同。

          對于簡單的函數(shù)來說,函數(shù)語法是很直接的。這個例子聲明了函數(shù) Abs,它接受一個類型為 T 的變量 x,并返回一個 float64 的值:

          func?Abs(x?T)?float64

          //?譯者補充調(diào)用示例:
          //?假定已經(jīng)初始化了一個變量?t,類型為?T,下同
          absT?:=?Abs(t)

          方法(method)只是有一個特殊參數(shù)的函數(shù),這個特殊參數(shù)就是它的接收者(receiver),可以通過點號 . 傳遞給函數(shù)。方法聲明的語法將接收者放在函數(shù)名前面的括號里。下面是同一個函數(shù),現(xiàn)在定義成 T 類型的方法:

          func?(x?T)?Abs()?float64

          //?譯者補充調(diào)用示例:
          absT?:=?t.Abs()

          而這里是一個函數(shù)變量(閉包),參數(shù)類型為 T;Go 有一等函數(shù)(first-class function)和閉包:

          譯者注:一等函數(shù)是指函數(shù)可以作為普通變量,可以作為其他函數(shù)的參數(shù)和返回值;作為對比, Java 只有類是一等公民,其他語言成分必須作為類的成員。

          negAbs?:=?func(x?T)?float64?{?return?-Abs(x)?}

          //?譯者補充調(diào)用示例:
          negT?:=?negAbs(t)

          最后,在 Go 里函數(shù)可以返回多個值。常見的做法是將函數(shù)結(jié)果和錯誤值作為一對返回,就像這樣:

          func?ReadByte()?(c?byte,?err?error)

          c,?err?:=?ReadByte()
          if?err?!=?nil
          ?{?...?}

          錯誤處理我們后面再聊。

          Go 缺少了一個特性,那就是它不支持函數(shù)的默認(rèn)參數(shù)(default function arguments)。這是一個故意的簡化。經(jīng)驗告訴我們,默認(rèn)參數(shù)會讓修復(fù) API 顯得太容易,仿佛只要添加更多參數(shù)就可以彌補設(shè)計上的缺陷,結(jié)果導(dǎo)致添加了過多的參數(shù),參數(shù)之間的關(guān)系變得難以拆分、甚至無法理解。缺少默認(rèn)參數(shù)的情況下,因為一個函數(shù)無法承載整個接口,就需要定義更多的函數(shù)或方法,但這會導(dǎo)致 API 更清晰、更容易理解。這些函數(shù)也都需要單獨命名,這使得有哪些函數(shù)、分別接受哪些參數(shù)一目了然,同時也鼓勵人們對命名進行更多的思考,這是清晰度和可讀性的一個關(guān)鍵方面。

          作為缺少默認(rèn)參數(shù)的補償,Go 支持易用的、類型安全的可變參數(shù)函數(shù)(variadic functions)。

          11. 命名

          Go 采用了一種不同尋常的方法來定義標(biāo)識符的可見性(所謂可見性,是指一個包的調(diào)用方是否可以通過標(biāo)識符使用包內(nèi)的成員)。不同于使用 privatepublic 等關(guān)鍵字,在 Go 里,命名本身就帶有信息:標(biāo)識符首字母的大小寫決定了標(biāo)識符的可見性。如果首字母是大寫字母,標(biāo)識符就會被導(dǎo)出(公共);否則就是私有的:

          • 首字母大寫:Name 對包的調(diào)用方可見
          • 首字母小寫:name (或 _Name)對包的調(diào)用方不可見

          這條規(guī)則適用于變量、類型、函數(shù)、方法、常量、字段 ...... 所有一切。這就是全部規(guī)則。

          這個設(shè)計不是一個容易做的決定。我們糾結(jié)了一年多的時間,去考慮用什么符號指定標(biāo)識符可見性。而一旦我們決定使用命名的大小寫,我們很快就意識到它已經(jīng)成為了語言里最重要的特性之一。名稱畢竟是給包的調(diào)用方使用的;把可見性放在名稱里而不是類型里,意味著只要看一眼,就能確定一個標(biāo)識符是否公共 API 的一部分 。在使用 Go 一段時間之后,再去看其他語言,還要查找聲明才能發(fā)現(xiàn)這些信息,就會覺得很累贅。

          目標(biāo)仍然是清晰度:程序源碼要簡單直接地表達(dá)程序員的意圖。

          另一個簡化是,Go 有一個非常緊湊的作用域(scope)層次結(jié)構(gòu):

          • 全局(預(yù)先聲明的標(biāo)識符,像 intstring
          • 包(包的所有源文件都在同一個作用域)
          • 文件(僅用于導(dǎo)入包的重命名,實踐中不是特別重要)
          • 函數(shù)(跟其它語言一樣)
          • 代碼塊(跟其它語言一樣)

          沒有什么命名空間(name space)作用域、類(class)作用域或者其它結(jié)構(gòu)的作用域。在 Go 里,名稱只來自很少的地方,而且所有名稱都遵循相同的作用域?qū)哟危涸谠创a的任意位置,一個標(biāo)識符只表示一個語言對象,和它的用法無關(guān)。(唯一的例外是語句標(biāo)簽 (用作 break 等語句的目標(biāo));它們總是具有函數(shù)作用域。)

          這使代碼更清晰。例如,請注意到方法聲明了一個顯式的接收者(explicit receiver),訪問該類型的字段和方法必須用到它。沒有隱式的(implicit) this 。也就是說,我們總是寫:

          rcvr.Field

          (其中 rcvr 是給接收者變量隨便起的名稱)所以在詞法上(lexically),該類型的所有元素,總是綁定到一個接收者類型的值上。類似地,對于導(dǎo)入的名稱,包的限定符總是存在;人們寫的是 io.Reader 而不是 Reader 。這樣不僅清楚,而且釋放了標(biāo)識符 Reader 作為一個有用的名稱,可以在任何包中使用。事實上,在標(biāo)準(zhǔn)庫中有多個導(dǎo)出的標(biāo)識符都叫 Reader,類似的還有很多 Printf,但具體引用了哪一個永遠(yuǎn)不會弄混。

          最后,這些規(guī)則結(jié)合在一起,保證除了頂層的預(yù)定義名稱如 int 之外,每個名稱(點號 . 前的第一部分)總是在當(dāng)前包中聲明。

          簡而言之,名稱總是本地的(local)。在 C、C++ 或 Java 里,名稱 y 可以指向任何東西。在 Go 里,y (甚至大寫的 Y )總是在包內(nèi)定義,而 x.Y 的解釋很清楚:在本地找到xY 就在里面。

          這些規(guī)則為可伸縮性提供了很重要的特性,因為它們保證了在一個包里添加導(dǎo)出的名稱永遠(yuǎn)不會破壞這個包的調(diào)用方。命名規(guī)則解耦了包,提供了可伸縮性、清晰度和健壯性。

          關(guān)于命名還有一個方面需要提及:方法查找總是只按名稱,而不是按方法的簽名(類型)。換句話說,一個類型永遠(yuǎn)不可能有兩個同名的方法。給定一個方法 x.M ,永遠(yuǎn)只有一個 Mx 關(guān)聯(lián)。同樣,這使得只給定名稱就能很容易地識別引用了哪個方法。這也使得方法調(diào)用的實現(xiàn)變得簡單。

          譯者注:換句話說,Go 不支持函數(shù)和方法重載。

          Go 的內(nèi)置函數(shù)其實是有重載的。makelen 這些函數(shù),參數(shù)類型不同,具體的行為也不一樣。make 甚至還有一個到三個參數(shù)的三個版本。這些函數(shù)根據(jù)參數(shù)不同,在編譯時被替換成了不同的函數(shù)實現(xiàn)。

          但為了保持代碼清晰,實現(xiàn)簡單和運行高效,Go 不支持用戶代碼的函數(shù)重載。

          12. 語義

          Go 語句的語義一般跟 C 語言類似。它是一種帶有指針等特性的、編譯型、靜態(tài)類型的過程式語言。設(shè)計上,習(xí)慣 C 族語言的程序員應(yīng)該會感到熟悉。在推出一門新語言時,目標(biāo)受眾能夠快速學(xué)會它是很重要的;將 Go 植根于 C 家族有助于確保年輕程序員能很容易學(xué)會 Go(他們大多數(shù)都知道 Java、JavaScript,也許還有 C)。

          盡管如此,Go 對 C 的語義還是做了很多小的改變,主要是出于健壯性的考慮。這些變化包括:

          • 沒有指針運算
          • 沒有隱式數(shù)字轉(zhuǎn)換
          • 總是檢查數(shù)組邊界
          • 沒有類型別名(聲明 type X int 之后, Xint 是不同的類型,而不是別名)
          • ++-- 是語句(statements)而不是表達(dá)式(expressions)
          • 賦值不是表達(dá)式
          • 對棧上變量取址是合法的(甚至是被鼓勵的)
          • 其它

          譯者注:

          1. Go 在 1.9 還是引入了類型別名,語法是 type X = int 。用來解決遷移、升級等重構(gòu)場景下,類型重命名的兼容性問題,以及方便引用外部導(dǎo)入的類型。

            實際上,類型別名僅在代碼中存在,編譯時會全部替換成實際的類型,不會產(chǎn)生新類型。

          2. 語句和表達(dá)式的差別是:語句是計算機角度的一個可執(zhí)行動作,不一定有值;表達(dá)式是數(shù)學(xué)角度的可求值算式,一定有值,這個值可以放在賦值符號的右邊,或者成為更大的表達(dá)式的一部分。

            不再區(qū)分語句和表達(dá)式,是編程語言演化的其中一個趨勢,這可以增強語言的表達(dá)能力。一般的做法,是增加求值規(guī)則(像語句的值是語句中最后一個表達(dá)式的值),給原本沒有值的語句提供一個值,這樣就可以通過拼接非常復(fù)雜的表達(dá)式,用很少的代碼解決問題。例如,如果賦值語句有值,那么 e = d = c = b = a = 10 ?就是合法的;因為賦值運算符從右到左結(jié)合,這些賦值最后都會成功,都是 10。

            但這很容易引起表達(dá)式的 濫用 和 誤用。人們有可能寫出非常難以理解的復(fù)雜表達(dá)式?;蛘咭驗椴皇煜つ承ū緛硎钦Z句的)表達(dá)式的求值規(guī)則而制造難以排查的錯誤。

            Go 首先追求代碼的清晰明確,而不是追求單純的表達(dá)能力強或者代碼行數(shù)少,所以反其道而行,反而去掉了某些語句的值。

          3. 棧上分配的內(nèi)存會在函數(shù)返回后被回收,對棧上的變量取址并返回,會導(dǎo)致函數(shù)外部引用到已被回收的內(nèi)存。這就是懸掛指針問題,困擾著大多數(shù)有指針的語言。Go 的解決方案是,在編譯期做逃逸分析,識別出可能超出當(dāng)前作用域的指針引用,將對應(yīng)的內(nèi)存分配到堆上。所以在 Go 里面,取址操作不用考慮變量究竟是棧上還是堆上的,編譯器會反過來配合你。當(dāng)然,如果是高頻操作,可能要考慮一下拷貝和 GC 哪個開銷大,傳值(棧上分配,需要拷貝,不需要 GC)還是 傳指針(如果發(fā)生逃逸,堆上分配,不需要拷貝,需要 GC)。

          還有一些更大的變化,遠(yuǎn)離了傳統(tǒng)的 C、C++ 甚至 Java 的模式。這些包括在語言級別上支持:

          • 并發(fā)
          • 垃圾回收
          • 接口類型
          • 反射
          • 類型判斷(type switches)

          下面的章節(jié)主要從軟件工程的角度簡要討論 Go 中的兩個主題:并發(fā) 和 垃圾回收。關(guān)于語言語義和用途的完整討論,請參見 golang.org 網(wǎng)站上的更多資料。

          13. 并發(fā)

          web 服務(wù)器運行在多核機器上,并有大量的調(diào)用方,這可以稱之為一個典型的 Google 程序;而并發(fā)對于這種現(xiàn)代計算環(huán)境非常重要。C++ 或 Java 都不是特別適合這類軟件,它們在語言層面上缺乏足夠的并發(fā)支持。

          Go 有作為一等公民的通道(channel),實現(xiàn)了 CSP (譯者注:Communicating Sequential Processes,通信順序進程)的一個變種。選擇 CSP 的部分原因是熟悉(我們其中一個人曾經(jīng)研究過某種基于 CSP 思想的前輩語言),同時也是因為 CSP 很容易被添加到過程化編程模型中,而無需對模型進行深入的修改。也就是說,給定一個類似于 C 的語言,CSP 基本就能夠以正交的方式添加到語言中,提供額外的表達(dá)能力,而不限制該語言的其他用途。 總之,語言的其他部分可以保持『普通』。

          這個方法就是,將獨立執(zhí)行的函數(shù),與其他普通的過程式代碼結(jié)合。

          這樣得到的語言允許我們將 并發(fā) 和 計算 平滑地結(jié)合起來。假設(shè)有一個 web 服務(wù)器,必須驗證每次客戶端調(diào)用的安全證書;在 Go 里面,很容易利用 CSP 來構(gòu)造這樣一個軟件:用獨立的執(zhí)行過程來管理客戶端,同時還能火力全開為昂貴的加密計算提供編譯型語言的高執(zhí)行效率。

          綜上所述,CSP 對于 Go 和 Google 來說都很實用。在編寫 web 服務(wù)器這種典型的 Go 程序時,這個模型是再適合不過了。

          有一個重要的注意事項:在并發(fā)的情況下,Go 并不是純粹的內(nèi)存安全(purely memory safe)語言。內(nèi)存共享是合法的,在通道上傳遞指針也是符合慣例的(同時也是高效的)。

          一些 并發(fā) 和 函數(shù)式編程 的專家對于 Go 在并發(fā)計算的上下文沒有采用『只寫一次(write-once)』來處理值語義感到失望,看起來沒有其它并發(fā)語言(如 Erlang)那么像回事。同樣地,原因主要還是在于對問題領(lǐng)域的熟悉度和適用性。Go 的并發(fā)特性在大多數(shù)程序員熟悉的上下文中都能很好地發(fā)揮作用。Go 可以實現(xiàn)簡單、安全的并發(fā)編程,但并不禁止不良的編程方式。 我們提供約定俗成的做法作為彌補,訓(xùn)練程序員將消息傳遞視為所有權(quán)控制的一種實現(xiàn)方式。我們的座右銘是:『不要通過共享內(nèi)存來通信,要通過通信來共享內(nèi)存』。

          譯者注:『只寫一次(write-once)』變量,在某些語言的實現(xiàn)里又叫『單次賦值(single-assignment)』變量(Erlang),或者『不可變(immutable)』變量(函數(shù)式編程)。換言之,這種變量只能在初始化時賦值(寫入)一次,之后不能再修改;如果需要新的值,只能創(chuàng)建新的變量。這樣可以避免在并發(fā)上下文意外修改了變量的值。

          雖然都不能修改,但還是要區(qū)分它和常量的區(qū)別。常量是在編譯期就已經(jīng)存在并確定了值;而不可變變量雖然賦值后不可修改,但其創(chuàng)建 / 賦值的時機和具體的值還是在運行時決定的。

          這其實是來自函數(shù)式編程『無副作用(side effect)』和『不修改狀態(tài)(state)』的概念,雖然可以保證程序的正確性,卻跟 C 家族的過程式編程模型差異很大,照搬過來需要對這個模型進行比較大的改動,這就違背 Go 的設(shè)計初衷了。

          從我們對 Go 和 并發(fā)編程 的新手程序員的有限了解來看,這是一種實用的做法。程序員享受著并發(fā)支持給網(wǎng)絡(luò)軟件帶來的簡單性,而簡單性產(chǎn)生了健壯性。

          譯者在網(wǎng)上看到一種說法:『Java 里多種同步方法、各種 Lock、并發(fā)調(diào)度等一系列復(fù)雜的功能在 Golang 里 都不存在,只靠 goroutine 和 channel 去處理并發(fā)?!?,這種說法是錯的。

          如上面所說,CSP 模型是以基本正交的方式添加到 C 家族的過程式編程模型里的,增加了新的、簡潔的表達(dá)方式,但并沒有限制原本的做法。

          Go 常用的并發(fā)控制的工具,除了內(nèi)置的消息通道 chan (CSP 模型),還有:

          • sync 包提供的同步原語(其中包括互斥鎖和讀寫互斥鎖 sync.Mutexsync.RWMutex,還有其它三個原語 sync.WaitGroup , sync.Oncesync.Cond 。實際上你去看 chan 的源碼,也是基于 runtime 內(nèi)部的 mutex 實現(xiàn)的);
          • 上下文 context.Context
          • 其它擴展包中提供的工具

          可以看到,在 C 家族里常見的并發(fā)控制方式,基本都有提供,只是不再像 Java 那樣以關(guān)鍵字的方式,而是以內(nèi)置包的方式提供。

          Go 把 CSP 模型實現(xiàn)并把支持上升到內(nèi)置類型和關(guān)鍵字的層面,卻并沒有強迫程序員必須使用這個模型。


          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包(下圖只是部分),同時還包含學(xué)習(xí)建議:入門看什么,進階看什么。

          關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲?。贿€可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。



          瀏覽 76
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产亚洲精品 码 | 一二三久久 | 亚洲热在线 | 天天操天天好逼网 | 国产精品国产精品国产 |