自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

用Go語(yǔ)言編寫一門工具的終極指南

開發(fā) 后端
我以前構(gòu)建過(guò)一個(gè)工具,以讓生活更輕松。這個(gè)工具被稱為: gomodifytags ,它會(huì)根據(jù)字段名稱自動(dòng)填充結(jié)構(gòu)體的標(biāo)簽字段。

我以前構(gòu)建過(guò)一個(gè)工具,以讓生活更輕松。這個(gè)工具被稱為: gomodifytags ,它會(huì)根據(jù)字段名稱自動(dòng)填充結(jié)構(gòu)體的標(biāo)簽字段。示例如下:

用Go語(yǔ)言編寫一門工具的***指南
(在 vim-go 中使用 gomodifytags 的一個(gè)用法示例)

使用這樣的工具可以 輕松管理 結(jié)構(gòu)體的多個(gè)字段。該工具還可以添加和刪除標(biāo)簽,管理標(biāo)簽選項(xiàng)(如omitempty),定義轉(zhuǎn)換規(guī)則(snake_case、camelCase 等)等等。但是這個(gè)工具是如何工作的? 在后臺(tái)中它究竟使用了哪些 Go 包? 有很多這樣的問題需要回答。

這是一篇非常長(zhǎng)的博客文章,解釋了如何編寫類似這樣的工具以及如何構(gòu)建它的每一個(gè)細(xì)節(jié)。 它包含許多特有的細(xì)節(jié)、提示和技巧和某些未知的 Go 位。

拿一杯咖啡,開始深入探究吧!

首先,列出這個(gè)工具需要完成的功能:

  • 它需要讀取源文件,理解并能夠解析 Go 文件
  • 它需要找到相關(guān)的結(jié)構(gòu)體
  • 找到結(jié)構(gòu)體后,需要獲取其字段名稱
  • 它需要根據(jù)字段名更新結(jié)構(gòu)標(biāo)簽(根據(jù)轉(zhuǎn)換規(guī)則,即:snake_case)
  • 它需要能夠使用這些改動(dòng)來(lái)更新文件,或者能夠以可接受的方式輸出改動(dòng)

我們首先來(lái)看看 結(jié)構(gòu)體標(biāo)簽的定義 是什么,之后我們會(huì)學(xué)習(xí)所有的部分,以及它們?nèi)绾谓M合在一起,從而構(gòu)建這個(gè)工具。

用Go語(yǔ)言編寫一門工具的***指南

結(jié)構(gòu)體的標(biāo)簽 值 (其內(nèi)容,比如`json:"foo"`)并 不是官方標(biāo)準(zhǔn)的一部分 ,不過(guò),存在一個(gè)非官方的規(guī)范,使用 reflect 包定義了其格式,這種方法也被 stdlib(例如 encoding/ json)包所采用。它是通過(guò) reflect.StructTag 類型定義的:

用Go語(yǔ)言編寫一門工具的***指南

結(jié)構(gòu)標(biāo)簽的定義比較簡(jiǎn)潔所以不容易理解。該定義可以分解如下:

  • 結(jié)構(gòu)標(biāo)簽是一個(gè)字符串(字符串類型)
  • 結(jié)構(gòu)標(biāo)簽的 Key 是非引號(hào)字符串
  • 結(jié)構(gòu)標(biāo)簽的 value 是一個(gè)帶引號(hào)的字符串
  • 結(jié)構(gòu)標(biāo)簽的 key 和 value 用冒號(hào)(:)分隔。冒號(hào)隔開的一個(gè) key 和對(duì)應(yīng)的 value 稱為 “key value 對(duì)”。
  • 一個(gè)結(jié)構(gòu)標(biāo)簽可以包含多個(gè) key valued 對(duì)(可選)。key-value 對(duì)之間用空格隔開。
  • 可選設(shè)置不屬于定義的一部分。類似 encoding/json 包將 value 解析為逗號(hào)分開的列表。value 的***個(gè)逗號(hào)后面的任何部分都是可選設(shè)置的一部分,例如:“ foo, omitempty,string”。其中 value 擁有一個(gè)叫 “foo” 的名字和可選設(shè)置 [“omitempty”, "string"]
  • 由于結(jié)構(gòu)標(biāo)簽是一個(gè)字符串,需要雙引號(hào)或者反引號(hào)包含。又因?yàn)?value 也需要引號(hào)包含,經(jīng)常用反引號(hào)包含結(jié)構(gòu)標(biāo)簽。

以上規(guī)則概況如下:

用Go語(yǔ)言編寫一門工具的***指南
(結(jié)構(gòu)標(biāo)簽的定義有許多隱含細(xì)節(jié))

已經(jīng)了解什么是結(jié)構(gòu)標(biāo)簽,接下來(lái)可以根據(jù)需要修改結(jié)構(gòu)標(biāo)簽。問題來(lái)了,如何才能很容易的對(duì)所做的修改進(jìn)行解析?很幸運(yùn),reflect.StructTag 包含一個(gè)可以解析結(jié)構(gòu)標(biāo)簽并返回特定 key 的 value 的方法。示例如下:

 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "reflect" 
  6.  
  7. func main() { 
  8.     tag := reflect.StructTag(`species:"gopher" color:"blue"`) 
  9.     fmt.Println(tag.Get("color"), tag.Get("species")) 

輸出:

  1. blue gopher 

如果 key 不存在則返回空串。

這是非常有幫助的, 但是 ,它有一些附加說(shuō)明,使其不適合我們,因?yàn)槲覀冃枰嗟撵`活性。這些是:

  • 它無(wú)法檢測(cè)到標(biāo)簽是否存在 格式錯(cuò)誤 (即:鍵被引用了,值是未引用等)
  • 它不知道選項(xiàng)的 語(yǔ)義
  • 它沒有辦法 迭代現(xiàn)有的標(biāo)簽 或返回它們。 我們必須知道我們要修改哪些標(biāo)簽。 如果不知道其名字怎么辦?
  • 修改現(xiàn)有標(biāo)簽是不可能的。
  • 我們不能重新 構(gòu)建新的struct標(biāo)簽 。

為了改進(jìn)這一點(diǎn),我編寫了一個(gè)自定義的Go包,它修復(fù)了上面的所有問題,并提供了一個(gè)可以輕松修改struct標(biāo)簽的每個(gè)方面的API。

用Go語(yǔ)言編寫一門工具的***指南

這個(gè)包被稱為 structtag ,并且可以從 github.com/fatih/structtag 獲取到。這個(gè)包允許我們以一種整潔的方式 解析和修改標(biāo)簽 。以下是一個(gè)完整的可工作的示例,復(fù)制/粘貼并自行嘗試下:

 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6.     "github.com/fatih/structtag" 
  7.  
  8. func main() { 
  9.     tag := `json:"foo,omitempty,string" xml:"foo"
  10.  
  11.     // parse the tag 
  12.     tags, err := structtag.Parse(string(tag)) 
  13.     if err != nil { 
  14.         panic(err) 
  15.     } 
  16.  
  17.     // iterate over all tags 
  18.     for _, t := range tags.Tags() { 
  19.         fmt.Printf("tag: %+v\n", t) 
  20.     } 
  21.  
  22.     // get a single tag 
  23.     jsonTag, err := tags.Get("json"
  24.     if err != nil { 
  25.         panic(err) 
  26.     } 
  27.  
  28.     // change existing tag 
  29.     jsonTag.Name = "foo_bar" 
  30.     jsonTag.Options = nil 
  31.     tags.Set(jsonTag) 
  32.  
  33.     // add new tag 
  34.     tags.Set(&structtag.Tag{ 
  35.         Key:     "hcl"
  36.         Name:    "foo"
  37.         Options: []string{"squash"}, 
  38.     }) 
  39.  
  40.     // print the tags 
  41.     fmt.Println(tags) // Output: json:"foo_bar" xml:"foo" hcl:"foo,squash" 

既然我們已經(jīng)知道如何解析一個(gè)struct標(biāo)簽了,以及修改它或創(chuàng)建一個(gè)新的,現(xiàn)在是時(shí)候來(lái)修改一個(gè)有效的Go源文件了。在上面的示例中,標(biāo)簽已經(jīng)存在了,但是如何從現(xiàn)有的Go結(jié)構(gòu)中獲取標(biāo)簽?zāi)?

簡(jiǎn)要回答:通過(guò) AST 。AST( Abstract Syntax Tree ,抽象語(yǔ)法樹)允許我們從源代碼中檢索每個(gè)單獨(dú)的標(biāo)識(shí)符(node)。下圖中你可以看到一個(gè)結(jié)構(gòu)類型的AST(簡(jiǎn)化版):

用Go語(yǔ)言編寫一門工具的***指南
(結(jié)構(gòu)體的基本的Go ast.Node 表示)

在這棵樹中,我們可以檢索和操縱每個(gè)標(biāo)識(shí)符,每個(gè)字符串和每個(gè)括號(hào)等。這些都由 AST 節(jié)點(diǎn)表示。例如,我們可以通過(guò)替換表示它的節(jié)點(diǎn)中的名字將字段名稱從“Foo”更改為“Bar”。相同的邏輯也適用于struct標(biāo)簽。

要 得到Go AST ,我們需要解析源文件并將其轉(zhuǎn)換為AST。實(shí)際上,這兩者都是通過(guò)一個(gè)步驟處理的。

要做到這一點(diǎn),我們將使用 go/parser 包來(lái) 解析 文件以獲取(整個(gè)文件的)AST,然后使用 go/ast 包來(lái)遍歷整棵樹(我們也可以手動(dòng)執(zhí)行, 但這是另一篇博文的主題)。下面代碼你可以看到一個(gè)完整的例子:

 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "go/ast" 
  6.     "go/parser" 
  7.     "go/token" 
  8.  
  9. func main() { 
  10.     src := `package main 
  11.         type Example struct { 
  12.     Foo string` + " `json:\"foo\"` }" 
  13.  
  14.     fset := token.NewFileSet() 
  15.     file, err := parser.ParseFile(fset, "demo", src, parser.ParseComments) 
  16.     if err != nil { 
  17.         panic(err) 
  18.     } 
  19.  
  20.     ast.Inspect(file, func(x ast.Node) bool { 
  21.         s, ok := x.(*ast.StructType) 
  22.         if !ok { 
  23.             return true 
  24.         } 
  25.  
  26.         for _, field := range s.Fields.List { 
  27.             fmt.Printf("Field: %s\n", field.Names[0].Name
  28.             fmt.Printf("Tag:   %s\n", field.Tag.Value) 
  29.         } 
  30.         return false 
  31.     }) 

上面代碼輸出如下:

 

  1. Field: Foo  
  2. Tag: `json:"foo"

上面代碼執(zhí)行以下操作:

  • 我們定義了僅包含一個(gè)結(jié)構(gòu)體的有效Go包的實(shí)例。
  • 我們使用 go/parser 包來(lái)解析這個(gè)字符串。解析器包也可以從磁盤讀取文件(或整個(gè)包)。
  • 在我們解析之后,我們保存我們的節(jié)點(diǎn)(分配給變量文件)并查找由 *ast.StructType 定義的AST節(jié)點(diǎn)(參見AST映像作為參考)。遍歷樹是通過(guò)ast.Inspect()函數(shù)完成的。它會(huì)遍歷所有節(jié)點(diǎn),直到它收到false值。這是非常方便的,因?yàn)樗恍枰烂總€(gè)節(jié)點(diǎn)。
  • 我們打印結(jié)構(gòu)體的字段名稱和結(jié)構(gòu)標(biāo)簽。

我們現(xiàn)在可以完成 兩件重要的事情了 ,首先,我們知道如何 解析一個(gè) Go 源文件 并檢索其中結(jié)構(gòu)體的標(biāo)簽(通過(guò)go/parser)。其次,我們知道 如何解析 Go 結(jié)構(gòu)體標(biāo)簽 ,并根據(jù)需要進(jìn)行修改(通過(guò) github.com/fatih/structtag )。

既然我們有了這些,我們可以通過(guò)使用這兩個(gè)重要的代碼片段開始構(gòu)建我們的工具(名為 gomodifytags )。該工具應(yīng)順序執(zhí)行以下操作:

  • 獲取配置,以識(shí)別我們要修改哪個(gè)結(jié)構(gòu)體
  • 根據(jù)配置查找和修改結(jié)構(gòu)體
  • 輸出結(jié)果

由于 gomodifytags 將主要由編輯器來(lái)執(zhí)行,我們打算通過(guò) CLI 標(biāo)志傳遞配置信息。第二步包含多個(gè)步驟,如解析文件、找到正確的結(jié)構(gòu)體,然后修改結(jié)構(gòu)(通過(guò)修改 AST 完成)。***,我們將輸出結(jié)果,或是按照原始的 Go 源文件或是某種自定義協(xié)議(如 JSON,稍后再說(shuō))。

以下是 gomodifytags 簡(jiǎn)化之后的主要功能:

用Go語(yǔ)言編寫一門工具的***指南

讓我們開始詳細(xì)解釋每個(gè)步驟。為了保持簡(jiǎn)單,我將嘗試以萃取形式解釋重要的部分。盡管一切都是一樣的,一旦你讀完了這篇博文,你將能夠在無(wú)需任何指導(dǎo)的情況下通讀整個(gè)源代碼(你將會(huì)在本指南的***找到所有資源)

讓我們從***步開始,了解如何 獲取配置 。以下是我們的配置文件,其中包含所有的必要信息

 

  1. type config struct { 
  2.     // first section - input & output 
  3.     file     string 
  4.     modified io.Reader 
  5.     output   string 
  6.     write    bool 
  7.  
  8.     // second section - struct selection 
  9.     offset     int 
  10.     structName string 
  11.     line       string 
  12.     start, end int 
  13.  
  14.     // third section - struct modification 
  15.     remove    []string 
  16.     add       []string 
  17.     override  bool 
  18.     transform string 
  19.     sort      bool 
  20.     clear     bool 
  21.     addOpts    []string 
  22.     removeOpts []string 
  23.     clearOpt   bool 

它分為 三個(gè) 主要部分:

***部分包含有關(guān)如何和哪個(gè)文件要讀入的配置。這可以是本地文件系統(tǒng)的文件名,也可以是直接來(lái)自stdin的數(shù)據(jù)(主要用在編輯器中)。它還設(shè)置了如何輸出結(jié)果(Go源文件或JSON形式),以及我們是否應(yīng)該覆寫文件,而不是輸出到stdout中。

第二部分定義了如何選擇一個(gè)結(jié)構(gòu)體及其字段。有多種方法可以做到這一點(diǎn)。我們可以通過(guò)它的偏移(光標(biāo)位置)、結(jié)構(gòu)名稱,單行(僅指定字段)或一系列行來(lái)定義它。***,我們總是需要得到起始行號(hào)。例如在下面的例子中,你可以看到一個(gè)例子,我們用它的名字來(lái)選擇結(jié)構(gòu)體,然后提取起始行號(hào),以便我們可以選擇正確的字段:

用Go語(yǔ)言編寫一門工具的***指南

而編輯器***使用 字節(jié)偏移量 。例如下面你可以看到我們的光標(biāo)剛好在“Port”字段名稱之后,從那里我們可以很容易地得到起始行號(hào):

用Go語(yǔ)言編寫一門工具的***指南

 

config配置中的 第三 部分實(shí)際上是一個(gè)到我們的 structtagpackage的 一對(duì)一的映射。它基本上允許我們?cè)谧x取字段后將配置傳遞給structtag包。如你所知,structtag包允許我們解析一個(gè)struct標(biāo)簽并在各個(gè)部分進(jìn)行修改。但是,它不會(huì)覆寫或更新結(jié)構(gòu)體的域值。

我們?cè)撊绾潍@得配置呢? 我們只需使用flag包,然后為配置中的每個(gè)字段創(chuàng)建一個(gè)標(biāo)志,然后給他們賦值。舉個(gè)例子:

 

  1. flagFile := flag.String("file""""Filename to be parsed"
  2. cfg := &config{ 
  3.     file: *flagFile, 

我們對(duì) 配置中的每個(gè)字段 執(zhí)行相同操作。相關(guān)完整的列表請(qǐng)查看gomodifytag的當(dāng)前master分支上的 flag 定義。

一旦我們有了配置,我們就可以做一些基本的驗(yàn)證了:

 

  1. func main() { 
  2.     cfg := config{ ... } 
  3.  
  4.     err := cfg.validate() 
  5.     if err != nil { 
  6.         log.Fatalln(err) 
  7.     } 
  8.  
  9.     // continue parsing 
  10.  
  11. // validate validates whether the config is valid or not 
  12. func (c *config) validate() error { 
  13.     if c.file == "" { 
  14.         return errors.New("no file is passed"
  15.     } 
  16.  
  17.     if c.line == "" && c.offset == 0 && c.structName == "" { 
  18.         return errors.New("-line, -offset or -struct is not passed"
  19.     } 
  20.  
  21.     if c.line != "" && c.offset != 0 || 
  22.         c.line != "" && c.structName != "" || 
  23.         c.offset != 0 && c.structName != "" { 
  24.         return errors.New("-line, -offset or -struct cannot be used together. pick one"
  25.     } 
  26.  
  27.     if (c.add == nil || len(c.add) == 0) && 
  28.         (c.addOptions == nil || len(c.addOptions) == 0) && 
  29.         !c.clear && 
  30.         !c.clearOption && 
  31.         (c.removeOptions == nil || len(c.removeOptions) == 0) && 
  32.         (c.remove == nil || len(c.remove) == 0) { 
  33.         return errors.New("one of " + 
  34.             "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" + 
  35.             " should be defined"
  36.     } 
  37.  
  38.     return nil 

將驗(yàn)證部分代碼放到一個(gè)單一的函數(shù)中,使得測(cè)試測(cè)試更簡(jiǎn)單。既然我們已經(jīng)知道如何獲取配置并進(jìn)行驗(yàn)證,我們繼續(xù)去解析文件:

用Go語(yǔ)言編寫一門工具的***指南

我們?cè)谝婚_始就討論了如何解析一個(gè)文件。這里解析的是config結(jié)構(gòu)體中的方法。實(shí)際上,所有的方法都是config結(jié)構(gòu)體的一部分:

 

  1. func main() { 
  2.     cfg := config{} 
  3.  
  4.     node, err := cfg.parse() 
  5.     if err != nil { 
  6.         return err 
  7.     } 
  8.  
  9.     // continue find struct selection ... 
  10.  
  11. func (c *config) parse() (ast.Node, error) { 
  12.     c.fset = token.NewFileSet() 
  13.     var contents interface{} 
  14.     if c.modified != nil { 
  15.         archive, err := buildutil.ParseOverlayArchive(c.modified) 
  16.         if err != nil { 
  17.             return nil, fmt.Errorf("failed to parse -modified archive: %v", err) 
  18.         } 
  19.         fc, ok := archive[c.file] 
  20.         if !ok { 
  21.             return nil, fmt.Errorf("couldn't find %s in archive", c.file) 
  22.         } 
  23.         contents = fc 
  24.     } 
  25.  
  26.     return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments) 

解析函數(shù)只完成了一件事。解析源碼并返回一個(gè)ast.Node。如果我們僅傳遞文件,這是非常簡(jiǎn)單的,在這種情況下,我們使用parser.ParseFile()函數(shù)。需要注意的是token.NewFileSet(),它創(chuàng)建一個(gè)類型為*token.FileSet。我們將它存儲(chǔ)在c.fset中,但也傳遞給parser.ParseFile()函數(shù)。為什么呢?

因?yàn)?fileset 用于獨(dú)立地為每個(gè)文件存儲(chǔ)每個(gè)節(jié)點(diǎn)的位置信息。這將在以后對(duì)于獲得ast.Node的確切信息非常有幫助(請(qǐng)注意,ast.Node使用一個(gè)緊湊的位置信息,稱為token.Pos。要獲取更多的信息,它需要通過(guò)token.FileSet.Position()函數(shù)來(lái)獲取一個(gè)token.Position,其中包含更多的信息)

讓我們繼續(xù)。如果通過(guò) stdin 傳遞源文件,它會(huì)變得更加有趣。config.modified 字段是易于測(cè)試的 io.Reader ,但實(shí)際上我們通過(guò) stdin 傳遞它。我們?nèi)绾螜z測(cè)是否需要從 stdin 讀取呢?

我們?cè)儐栍脩羰欠?想 通過(guò) stdin 傳遞內(nèi)容。在這種情況下,本工具的用戶需要傳遞--modified 標(biāo)志(這是一個(gè) 布爾 標(biāo)志)。如果用戶傳遞了該標(biāo)志,我們只需將 stdin 分配給 c.modified 即可:

 

  1. flagModified = flag.Bool("modified"false
  2.     "read an archive of modified files from standard input"
  3.  
  4. if *flagModified { 
  5.     cfg.modified = os.Stdin 

如果你再次檢查上面的 config.parse() 函數(shù),你將看到我們檢查 .modified 字段是否已分配,因?yàn)?stdin 是一個(gè)任意數(shù)據(jù)的流,我們需要能夠根據(jù)給定的協(xié)議對(duì)其進(jìn)行解析。在這種情況下,我們假定其中包含以下內(nèi)容:

  • 文件名,后跟換行符
  • (十進(jìn)制)文件大小,后跟換行符
  • 文件的內(nèi)容

因?yàn)槲覀冎牢募笮?,我們可以毫無(wú)問題地解析此文件的內(nèi)容。任何大于給定文件大小的部分,我們僅需停止解析。

這種 方法 也被其他幾種工具所使用(如 guru、gogetdoc 等),并且它對(duì)編輯器來(lái)說(shuō)是非常有用的。因?yàn)檫@樣可以讓編輯器傳遞修改后的文件內(nèi)容, 并且無(wú)需保存到文件系統(tǒng)中 。因此它被命名為“modified”。

既然我們已經(jīng)擁有了 Node ,讓我們繼續(xù)下一步的“查找結(jié)構(gòu)體”:

用Go語(yǔ)言編寫一門工具的***指南

我們的主函數(shù)中,我們將使用在上一步中解析的 ast.Node 中調(diào)用 findSelection() 函數(shù):

 

  1. func main() { 
  2.     // ... parse file and get ast.Node  
  3.     start, end, err := cfg.findSelection(node) 
  4.     if err != nil { 
  5.         return err 
  6.     }  
  7.     // continue rewriting the node with the start&end position 

cfg.findSelection() 函數(shù)會(huì)根據(jù)配置文件和我們選定結(jié)構(gòu)體的方式來(lái)返回指定結(jié)構(gòu)體的開始和結(jié)束位置。它在給定 Node 上進(jìn)行迭代,然后返回其起始位置(和以上的配置一節(jié)中的解釋類似):

用Go語(yǔ)言編寫一門工具的***指南

(檢索步驟會(huì)迭代所有 node ,直到其找到一個(gè) *ast.StructType ,然后返回它在文件中的起始位置。)

責(zé)任編輯:未麗燕 來(lái)源: 開源中國(guó)翻譯文章
相關(guān)推薦

2019-11-18 11:00:58

程序員編程語(yǔ)言

2015-07-28 15:35:48

學(xué)習(xí)語(yǔ)言

2022-07-25 19:48:47

Go

2014-12-03 09:48:36

編程語(yǔ)言

2012-03-28 09:40:40

JavaScript

2011-12-30 09:33:02

程序員語(yǔ)言

2021-07-09 06:48:30

語(yǔ)言Scala編程

2012-09-04 11:20:31

2022-02-27 14:45:16

編程語(yǔ)言JavaC#

2022-11-04 11:11:15

語(yǔ)言入職項(xiàng)目

2014-09-26 09:29:12

Python

2017-04-07 16:49:00

語(yǔ)言程序編程

2017-04-07 10:45:43

編程語(yǔ)言

2024-06-27 09:00:00

人工智能編程語(yǔ)言軟件開發(fā)

2020-09-27 15:52:02

編程語(yǔ)言C 語(yǔ)言Python

2023-02-08 07:35:43

Java語(yǔ)言面向?qū)ο?/a>

2022-02-21 11:15:59

編程語(yǔ)言后端開發(fā)

2022-09-07 08:05:32

GScript?編程語(yǔ)言

2011-07-14 17:58:11

編程語(yǔ)言

2018-07-16 12:36:48

編程語(yǔ)言PythonJava
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)