Go向けの完全なユーティリティ作成ガイド







少し前に、私は私の人生を簡素化するユーティリティを作り始めました。 これはgomodifytagsと呼ばれます 。 ユーティリティは、フィールド名を使用して構造タグ(structタグ)のフィールドに自動的に入力します。 例:







vim-goでgomodifytagsを使用する例







このユーティリティを使用すると、複数の構造フィールドを簡単に管理できます。 タグの追加と削除、オプションの管理(たとえば、emptyempty)、変換ルール( snake_case、camelCaseなど)の定義などができます。 このユーティリティはどのように機能しますか? 彼女はどのGoパックを使用していますか? おそらく多くの質問があります。







この非常に長い記事では、このようなユーティリティを作成および構築する方法について詳しく説明します。 ここには、Goコードだけでなく、多くのヒントやコツがあります。







コーヒーを注いで、読み始めましょう!







まず、ユーティリティが何をすべきかを考えましょう。







  1. ソースファイルを読んで理解し、Goファイルに変換します。
  2. 適切な構造を見つけます。
  3. フィールド名を取得します。
  4. 構造タグをフィールド名で更新します(変換ルール、たとえばsnake_caseに従って )。
  5. 最後に、これらの変更をファイルに加えるか、別の便利な方法でプログラマーに提供します。


構造タグを定義することから始め、次にユーティリティを順番に構築して、その部分と相互の相互作用を調べます。













タグ(たとえば、そのコンテンツjson:"foo"



)は、公式の仕様には記載されていません 。 ただし、reflectパッケージには、stdlibパッケージも使用する形式( encoding / jsonなど )でこのタグを定義する非公式の仕様があります。 これは、 reflect.StructTag型を介して行われます













定義は簡単ではありません。理解しましょう。









これらすべてのルールを繰り返します。









構造タグの定義には多くの明白でない詳細があります。







これで、構造タグが何であるかがわかり、必要に応じて簡単に変更できます。 質問は、今それを解析して簡単に変更する方法ですか? 幸い、 reflect.StructTagには、タグを解析して特定のキーの値を返すメソッドもあります。 例:







 package main import ( "fmt" "reflect" ) func main() { tag := reflect.StructTag(`species:"gopher" color:"blue"`) fmt.Println(tag.Get("color"), tag.Get("species")) }
      
      





画面に表示します:







 blue gopher
      
      





キーが存在しない場合、空の文字列が返されます。







しかし、非常に便利ですが、より柔軟なソリューションが必要なため、この機能が役に立たない制限があります。 制限のリスト:









Goパッケージを作成しました。これは、これらすべての欠点を修正し、構造タグの任意の部分を簡単に変更できるAPIを提供します。













パッケージはstructtagと呼ばれ、ここからダウンロードできます: github.com/fatih/structtag パッケージは、タグを正確に解析および変更できます 。 以下は完全に機能する例です。コピーしてテストできます。







 package main import ( "fmt" "github.com/fatih/structtag" ) func main() { tag := `json:"foo,omitempty,string" xml:"foo"` // parse the tag tags, err := structtag.Parse(string(tag)) if err != nil { panic(err) } // iterate over all tags for _, t := range tags.Tags() { fmt.Printf("tag: %+v\n", t) } // get a single tag jsonTag, err := tags.Get("json") if err != nil { panic(err) } // change existing tag jsonTag.Name = "foo_bar" jsonTag.Options = nil tags.Set(jsonTag) // add new tag tags.Set(&structtag.Tag{ Key: "hcl", Name: "foo", Options: []string{"squash"}, }) // print the tags fmt.Println(tags) // Output: json:"foo_bar" xml:"foo" hcl:"foo,squash" }
      
      





これで、構造タグを解析し、変更して新しいタグを作成できます。 次に、正しいソースGoファイルを変更する必要があります。 上記の例では、タグはすでにそこにありますが、既存のGo構造からどのように取得しますか?







回答: ASTを通じて。 AST( 抽象構文ツリー )を使用すると、ソースコードから任意の識別子(ノード)を抽出できます。 以下は、単純化された構造化ツリーです。









Go ast.Node構造タイプの基本表現







このツリーでは、任意の識別子を抽出して操作できます-文字列、角括弧など。それらはそれぞれASTノードで表されます 。 たとえば、フィールドの名前を「Foo」から「Bar」に変更して、それを表すノードを置き換えることができます。 構造タグについても同じです。







Go ASTを取得するには、ソースファイルを解析し、ASTに変換する必要があります。 これらはすべて1ステップで実行されます。







ファイルを解析するには、 go / parserパッケージを使用して(ファイル全体のツリーを構築します)、 go / astパッケージの助けを借りてツリーを調べます(手動で行うことができますが、これは別の記事のトピックです) 完全に機能する例を次に示します。







 package main import ( "fmt" "go/ast" "go/parser" "go/token" ) func main() { src := `package main type Example struct { Foo string` + " `json:\"foo\"` }" fset := token.NewFileSet() file, err := parser.ParseFile(fset, "demo", src, parser.ParseComments) if err != nil { panic(err) } ast.Inspect(file, func(x ast.Node) bool { s, ok := x.(*ast.StructType) if !ok { return true } for _, field := range s.Fields.List { fmt.Printf("Field: %s\n", field.Names[0].Name) fmt.Printf("Tag: %s\n", field.Tag.Value) } return false }) }
      
      





実行結果:







 Field: Foo Tag: `json:"foo"`
      
      





私たちがすること:









これで、 2つの重要な問題を解決できます 。 最初に、 元のGoファイルを解析し 、構造タグを取得する方法を知っています (go / parserを使用)。 次に、 構造タグ解析し、必要に応じて変更できます( github.com/fatih/structtagを使用)。







これらの2つのスキルを使用して、ユーティリティ(gomodifytags)の構築を開始できます。 彼女は:







  1. どの構造を変更する必要があるかを示す構成を取得します。
  2. この構造を見つけて変更します。
  3. 結果を出力します。


gomodifytagsはほとんどの場合エディターによって実行されるため、CLIフラグを介して構成を渡します。 2番目の段階は、ファイルの解析、正しい構造の検索、変更(ASTを使用)などのいくつかの手順で構成されます。 最後に、結果を元のGoファイルに出力するか、何らかのプロトコル(JSONなど)で出力します。これについては以下で説明します。







gomodifytagsの簡略化されたメイン関数:













各ステップを詳しく見てみましょう。 もっと簡単に伝えようと思います。 ここではすべてが同じですが、読み終えると、ヘルプなしでソースコードを把握できます(ソースへのリンクはマニュアルの最後にあります)。







設定を取得することから始めましょう。 以下は、必要なすべての情報がある構成です。







 type config struct { // first section - input & output file string modified io.Reader output string write bool // second section - struct selection offset int structName string line string start, end int // third section - struct modification remove []string add []string override bool transform string sort bool clear bool addOpts []string removeOpts []string clearOpt bool }
      
      





構成は3つのセクションで構成されています。







最初のファイルには、どのファイルをどのように読み込むかを説明する設定が含まれています。 これは、ローカルファイルシステムのファイル名を使用するか、stdinから直接実行できます(通常はエディターで作業するときに使用されます)。 最初のセクションでは、結果を(元のGoファイルまたはJSONに)出力する方法と、stdoutに出力する代わりにファイルを上書きするかどうかも示します。







2番目のセクションでは、構造とそのフィールドを選択する方法を示します。 これはさまざまな方法で実行できます。オフセット(カーソル位置)、構造の名前、1行(フィールドを選択するだけ)、または行の範囲を使用します。 最後に、開始行と終了行を抽出する必要があります。 以下は、名前で構造を選択し、開始行と終了行を抽出して正しいフィールドを選択する方法を示しています。













同時に、エディターはバイトオフセットを使用する方が快適です。 次の図では、カーソルは"Port"



フィールドの名前のすぐ後にあり、そこから開始行と終了行を簡単に取得できます。













構成の3番目のセクションは、 structtag



パッケージにデータを渡すことにstructtag



ます。 すべてのフィールドを読み取った後、構成がstructtag



パッケージに渡されます。これにより、構造タグを解析し、その部分を変更できます。 ただし、同時に、構造フィールドは上書きまたは更新されません。







そして、どのように構成を取得しますか? flag



パッケージを取得し、構成内の各フィールドのフラグを作成し、それらにバインドします。 例:







 flagFile := flag.String("file", "", "Filename to be parsed") cfg := &config{ file: *flagFile, }
      
      





構成内の各フィールドに対して同じことを行いますフラグ定義の完全なリストは、現在のgomodifytagウィザードにあります。







設定を受け取ったら、基本的なチェックを実行します。







 func main() { cfg := config{ ... } err := cfg.validate() if err != nil { log.Fatalln(err) } // continue parsing } // validate validates whether the config is valid or not func (c *config) validate() error { if c.file == "" { return errors.New("no file is passed") } if c.line == "" && c.offset == 0 && c.structName == "" { return errors.New("-line, -offset or -struct is not passed") } if c.line != "" && c.offset != 0 || c.line != "" && c.structName != "" || c.offset != 0 && c.structName != "" { return errors.New("-line, -offset or -struct cannot be used together. pick one") } if (c.add == nil || len(c.add) == 0) && (c.addOptions == nil || len(c.addOptions) == 0) && !c.clear && !c.clearOption && (c.removeOptions == nil || len(c.removeOptions) == 0) && (c.remove == nil || len(c.remove) == 0) { return errors.New("one of " + "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" + " should be defined") } return nil }
      
      





チェックが1つの関数で実行されると、テストが簡単になります。







ファイルの解析に移りましょう:













最初に、ファイルの解析方法についてすでに説明しました。 この場合、解析は構成構造内のメソッドによって行われます。 実際、すべてのメソッドはこの構造の一部です。







 func main() { cfg := config{} node, err := cfg.parse() if err != nil { return err } // continue find struct selection ... } func (c *config) parse() (ast.Node, error) { c.fset = token.NewFileSet() var contents interface{} if c.modified != nil { archive, err := buildutil.ParseOverlayArchive(c.modified) if err != nil { return nil, fmt.Errorf("failed to parse -modified archive: %v", err) } fc, ok := archive[c.file] if !ok { return nil, fmt.Errorf("couldn't find %s in archive", c.file) } contents = fc } return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments) }
      
      





parse



関数はparse



ソースコードのみをparse



ast.Node



を返すことがast.Node



ます。 ファイルを転送する場合、すべてが非常に簡単です。この場合、 parser.ParseFile()



関数が使用されます。 *token.FileSet



タイプを作成するtoken.NewFileSet()



注意してください。 c.fset



に保存しますが、 parser.ParseFile()



関数にも渡します。 なんで?







ファイルセットは 、各ファイルごとに各ノードの場所に関する情報を保存するために使用されるためです。 これは、 ast.Node



正確な位置を取得するために後で非常に役立ちます。 (ast.Nodeはtoken.Posと呼ばれるコンパクトな位置情報を使用することに注意してください token.FileSet.Position()



関数を使用して token.FileSet.Position()



を復号化すると詳細情報含む token.Position



を取得ます。)







続けましょう。 stdinを介してソースファイルを転送すると、状況はさらに興味深いものになります。 config.modified



フィールドは簡単なテストのためにio.Readerですが、実際には標準入力を渡します。 しかし、stdinから何を読みたいのかを判断する方法は?







ユーザーにstdin経由でコンテンツを送信するかどうかを尋ねます。 この場合、ユーザーは--modified



フラグ( ブールフラグ)を渡す必要があります。 合格した場合は、 c.modified



stdinを添付しc.modified









 flagModified = flag.Bool("modified", false, "read an archive of modified files from standard input") if *flagModified { cfg.modified = os.Stdin }
      
      





config.parse()



関数をもう一度見ると、 .modified



フィールドの添付ファイルをチェックしていることが.modified



ます。 Stdinは、選択したプロトコルに従って解析する必要がある任意のデータストリームです。 この場合、アーカイブには次のものが含まれていると仮定します。









ファイルサイズがわかっているため、コンテンツを安全に解析できます。 何かが大きくなった場合は、解析を停止してください。







このアプローチは他のいくつかのユーティリティ( gurugogetdocなど)で使用されます。編集者はファイルシステムに保存せずに変更されたファイルの内容を転送できるため、編集者にとって非常に便利です。 したがって、「変更」。







したがって、ノードがあります。構造を探しましょう。













メイン関数では、 findSelection()



関数を呼び出します。これは前のステップで解析します。







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





cfg.findSelection()



関数は、構成に基づいて、構造の開始位置と終了位置、および構造が選択された順序を返します。 指定されたノードを通過し、開始位置と終了位置を返します(構成のセクションで説明されています)。













その後、関数は* ast.StructTypeが見つかるまですべてのノードを反復処理し、その開始位置と終了位置をファイルに返します。







しかし、これはどのように行われますか? 3つのモードがあることに注意してください。 オフセット、および構造名による選択:







 // findSelection returns the start and end position of the fields that are // suspect to change. It depends on the line, struct or offset selection. func (c *config) findSelection(node ast.Node) (int, int, error) { if c.line != "" { return c.lineSelection(node) } else if c.offset != 0 { return c.offsetSelection(node) } else if c.structName != "" { return c.structSelection(node) } else { return 0, 0, errors.New("-line, -offset or -struct is not passed") } }
      
      





行で選択するのが最も簡単です。 フラグ値自体を返すだけです。 ユーザーがフラグ"--line 3,50"



渡した場合、関数は(3, 50, nil)



"--line 3,50"



(3, 50, nil)



返します。 値を選択して整数に変換するだけでなく、同時にチェックします:







 func (c *config) lineSelection(file ast.Node) (int, int, error) { var err error splitted := strings.Split(c.line, ",") start, err := strconv.Atoi(splitted[0]) if err != nil { return 0, 0, err } end := start if len(splitted) == 2 { end, err = strconv.Atoi(splitted[1]) if err != nil { return 0, 0, err } } if start > end { return 0, 0, errors.New("wrong range. start line cannot be larger than end line") } return start, end, nil }
      
      





このモードは、編集者が行のグループを選択して選択(強調表示)するときに使用されます。







オフセットおよび構造名で選択するには、さらに作業が必要です。 まず、利用可能なすべての構造を収集して、オフセットを計算したり、名前で検索したりする必要があります。 したがって、すべての構造を収集します。







 // collectStructs collects and maps structType nodes to their positions func collectStructs(node ast.Node) map[token.Pos]*structType { structs := make(map[token.Pos]*structType, 0) collectStructs := func(n ast.Node) bool { t, ok := n.(*ast.TypeSpec) if !ok { return true } if t.Type == nil { return true } structName := t.Name.Name x, ok := t.Type.(*ast.StructType) if !ok { return true } structs[x.Pos()] = &structType{ name: structName, node: x, } return true } ast.Inspect(node, collectStructs) return structs }
      
      





ast.Inspect()



関数は、ASTを下って構造を探します。







最初に、名前を抽出できるように*ast.TypeSpec



が必要です。 *ast.StructType



を検索すると、構造自体はわかりますが、名前はわかりません。 したがって、名前と構造ノードを含むstructType



タイプを使用します。これは便利です。 各構造の位置は一意であるため、その位置をバインドのキーとして使用します。







これですべての構造ができました。オフセットと構造名を使用して、モードの開始位置と終了位置を返すことができます。 最初のケースでは、変位が指定された構造内に収まるかどうかを確認します。







 func (c *config) offsetSelection(file ast.Node) (int, int, error) { structs := collectStructs(file) var encStruct *ast.StructType for _, st := range structs { structBegin := c.fset.Position(st.node.Pos()).Offset structEnd := c.fset.Position(st.node.End()).Offset if structBegin <= c.offset && c.offset <= structEnd { encStruct = st.node break } } if encStruct == nil { return 0, 0, errors.New("offset is not inside a struct") } // offset mode selects all fields start := c.fset.Position(encStruct.Pos()).Line end := c.fset.Position(encStruct.End()).Line return start, end, nil }
      
      





collectStructs()



を使用して、構造を収集し、繰り返します。 ファイルの解析に使用した初期token.FileSet



を保存したことを覚えていますか?







これで、構造体の各ノードからオフセットに関する情報を取得するのに役立ちます( token.Position



復号化し .Offset



フィールドを取得します )。 構造(この場合、名前はencStruct



)が見つかるまで、チェックして繰り返します。







 for _, st := range structs { structBegin := c.fset.Position(st.node.Pos()).Offset structEnd := c.fset.Position(st.node.End()).Offset if structBegin <= c.offset && c.offset <= structEnd { encStruct = st.node break } }
      
      





この情報を使用して、見つかった構造の開始位置と終了位置を抽出できます。







 start := c.fset.Position(encStruct.Pos()).Line end := c.fset.Position(encStruct.End()).Line
      
      





構造名を選択するときに同じロジックを使用します。 オフセットが指定された構造内にあるかどうかをチェックする代わりに、正しい構造が見つかるまで構造の名前チェックします。







 func (c *config) structSelection(file ast.Node) (int, int, error) { // ... for _, st := range structs { if st.name == c.structName { encStruct = st.node } } // ... }
      
      





開始位置と終了位置を取得したら、構造フィールドの変更に進みます。













メイン関数では、前の手順で解析されたノードでcfg.rewrite()



関数を呼び出します。







 func main() { // ... find start and end position of the struct to be modified rewrittenNode, errs := cfg.rewrite(node, start, end) if errs != nil { if _, ok := errs.(*rewriteErrors); !ok { return errs } } // continue outputting the rewritten node }
      
      





これはユーティリティの重要な部分です。 書き換え機能は、開始位置と終了位置の間のすべての構造のフィールドを書き換えます。







 // rewrite rewrites the node for structs between the start and end // positions and returns the rewritten node func (c *config) rewrite(node ast.Node, start, end int) (ast.Node, error) { errs := &rewriteErrors{errs: make([]error, 0)} rewriteFunc := func(n ast.Node) bool { // rewrite the node ... } if len(errs.errs) == 0 { return node, nil } ast.Inspect(node, rewriteFunc) return node, errs }
      
      





ご覧のとおり、再びast.Inspect()



を使用して、指定されたノードのツリーを下に移動します。 rewriteFunc



関数内で、各フィールドのタグを書き換えます(これについては後で説明します)。







ast.Inspect()



によって返される関数はエラーを返さないため、エラースキーム( errs



変数を使用して定義される)を作成し、ツリーを下ってフィールドを処理してエラースキームを収集します。 rewriteFunc



対処しましょう。







 rewriteFunc := func(n ast.Node) bool { x, ok := n.(*ast.StructType) if !ok { return true } for _, f := range x.Fields.List { line := c.fset.Position(f.Pos()).Line if !(start <= line && line <= end) { continue } if f.Tag == nil { f.Tag = &ast.BasicLit{} } fieldName := "" if len(f.Names) != 0 { fieldName = f.Names[0].Name } // anonymous field if f.Names == nil { ident, ok := f.Type.(*ast.Ident) if !ok { continue } fieldName = ident.Name } res, err := c.process(fieldName, f.Tag.Value) if err != nil { errs.Append(fmt.Errorf("%s:%d:%d:%s", c.fset.Position(f.Pos()).Filename, c.fset.Position(f.Pos()).Line, c.fset.Position(f.Pos()).Column, err)) continue } f.Tag.Value = res } return true }
      
      





この関数はAST ノードごとに呼び出されることに注意してください。 したがって、タイプ*ast.StructType



ノードのみを探しています。 次に、構造体のフィールドを反復処理し始めます。







ここで、お気に入りの変数start



end



を再び使用します。 このコードは、フィールドを変更するかどうかを決定します。 彼の位置がstart—end



間にある場合は続行し、そうでない場合は注意を払いません。







 if !(start <= line && line <= end) { continue // skip processing the field }
      
      





次に、タグがあるかどうかを確認します。 タグフィールドが空( nil



)の場合、空のタグで初期化します。 後でこれはcfg.process()



関数の混乱を避けるのに役立ちます:







 if f.Tag == nil { f.Tag = &ast.BasicLit{} }
      
      





続行する前に、面白いことを説明しましょう。 gomodifytagsはフィールド名を取得して処理しようとします。 そして、フィールドが匿名の場合はどうなりますか?







 type Bar string type Foo struct { Bar //this is an anonymous field }
      
      





この場合、フィールドには名前がなく 、タイプ名に基づいてフィールド名を想定します。







 // if there is a field name use it fieldName := "" if len(f.Names) != 0 { fieldName = f.Names[0].Name } // if there is no field name, get it from type's name if f.Names == nil { ident, ok := f.Type.(*ast.Ident) if !ok { continue } fieldName = ident.Name }
      
      





フィールド名とタグ値を受け取ったら、フィールドの処理を開始できます。 cfg.process()



関数が処理を担当します(フィールド名とタグ値がある場合)。 結果(この場合、構造タグのフォーマット)を返します。これを使用して、既存のタグ値を上書きします。







 res, err := c.process(fieldName, f.Tag.Value) if err != nil { errs.Append(fmt.Errorf("%s:%d:%d:%s", c.fset.Position(f.Pos()).Filename, c.fset.Position(f.Pos()).Line, c.fset.Position(f.Pos()).Column, err)) continue } // rewrite the field with the new result,ie: json:"foo" f.Tag.Value = res
      
      





structtagを覚えている場合、実際にはString()がここに返されます-タグインスタンスの表現です。 タグの最終表現を返す前に、structtagパッケージのさまざまなメソッドを使用して、必要に応じて構造を変更します。 簡略化されたレビュー:









structtag







, removeTags()



process()



. ( ), :







 flagRemoveTags = flag.String("remove-tags", "", "Remove tags for the comma separated list of keys") if *flagRemoveTags != "" { cfg.remove = strings.Split(*flagRemoveTags, ",") }
      
      





removeTags()



, - --remove-tags



. tags.Delete()



structtag:







 func (c *config) removeTags(tags *structtag.Tags) *structtag.Tags { if c.remove == nil || len(c.remove) == 0 { return tags } tags.Delete(c.remove...) return tags }
      
      





cfg.Process()



.







, : .













cfg.format()



, :







 func main() { // ... rewrite the node out, err := cfg.format(rewrittenNode, errs) if err != nil { return err } fmt.Println(out) }
      
      





stdout . . -, — , . -, stdout , , .







format()



:







 func (c *config) format(file ast.Node, rwErrs error) (string, error) { switch c.output { case "source": // return Go source code case "json": // return a custom JSON output default: return "", fmt.Errorf("unknown output mode: %s", c.output) } }
      
      





.

(« ») ast.Node



Go-. , , .







(“JSON”) ( ). :







 type output struct { Start int `json:"start"` End int `json:"end"` Lines []string `json:"lines"` Errors []string `json:"errors,omitempty"` }
      
      





( ):













format()



. , . « » go/format AST Go-. , gofmt . « »:







 var buf bytes.Buffer err := format.Node(&buf, c.fset, file) if err != nil { return "", err } if c.write { err = ioutil.WriteFile(c.file, buf.Bytes(), 0) if err != nil { return "", err } } return buf.String(), nil
      
      





format io.Writer



. ( var buf bytes.Buffer



), , -write



. , Go.







JSON . , , . . , format.Node()



, lossy .







lossy- ? :







 type example struct { foo int // this is a lossy comment bar int }
      
      





*ast.Field



. *ast.Field.Comment



, .







? foo bar ?







, . format.Node()



, :







 type example struct { foo int bar int }
      
      





, lossy- *ast.File



. , . , , , JSON:







 var buf bytes.Buffer err := format.Node(&buf, c.fset, file) if err != nil { return "", err } var lines []string scanner := bufio.NewScanner(bytes.NewBufferString(buf.String())) for scanner.Scan() { lines = append(lines, scanner.Text()) } if c.start > len(lines) { return "", errors.New("line selection is invalid") } out := &output{ Start: c.start, End: c.end, Lines: lines[c.start-1 : c.end], // cut out lines } o, err := json.MarshalIndent(out, "", " ") if err != nil { return "", err } return string(o), nil
      
      





.







以上です!







, , :













gomodifytags







:









gomodifytags , :









ソースコード







Gophercon 2017








All Articles