Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14106] ファイルの概要

このコミットは、Go言語の標準ライブラリであるgo/printerパッケージにおけるコメントのフォーマット処理の改善に焦点を当てています。特に、コメントのフォーマットが「冪等(idempotent)」になるように修正が加えられています。これは、同じコードに対して複数回フォーマッタを適用しても、結果が常に同じになることを目指すものです。また、冪等性テストを容易にするためのテストフレームワークのリファクタリングも含まれています。

コミット

commit 241b23606c4c1d37071669c6e07c1918501835ff
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Oct 9 17:08:09 2012 -0700

    go/printer: idempotent comment formatting
    
    Also:
    
    - Refactored testing framework to permit easier
    idempotency testing.
    
    - Applied gofmt -w src misc
    
    This CL depends on CL 6639044 being applied first.
    
    Formatting is not idempotent for all files: In those
    files the comment position has changed (due to missing
    precise location information) and/or the comment formatting
    cannot/is not aware of independent code re-formatting.
    In general it is very hard to make format idempotent when
    running it in one pass only. Leaving that aside for now.
    
    Fixes #1835.
    
    R=r, rsc
    CC=golang-dev
    https://golang.org/cl/6624051

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/241b23606c4c1d37071669c6e07c1918501835ff

元コミット内容

このコミットの主な目的は、go/printerパッケージにおけるコメントのフォーマット処理を冪等にすることです。具体的には、以下の変更が含まれています。

  • コメントの冪等なフォーマット処理の実装。
  • 冪等性テストを容易にするためのテストフレームワークのリファクタリング。
  • gofmt -w src miscの適用による、ソースコード全体のフォーマット調整。

コミットメッセージには、すべてのファイルでフォーマットが冪等になるわけではないという制約も明記されています。これは、コメントの位置情報が不足している場合や、コードの再フォーマットとコメントのフォーマットが独立して行われる場合に発生する可能性があり、一度のパスで完全に冪等性を達成することは非常に困難であると述べられています。

この変更は、GoのIssue #1835を修正するものです。

変更の背景

Go言語には、コードを自動的にフォーマットするgofmtというツールがあります。gofmtは、Goのコードベース全体で一貫したスタイルを強制し、コードの可読性を高める上で非常に重要な役割を果たしています。しかし、初期のgofmtには、コメントのフォーマットに関していくつかの課題がありました。特に、同じコードに対してgofmtを複数回実行した場合に、コメントのフォーマットが毎回微妙に変化してしまうという「非冪等性」の問題が指摘されていました。

この非冪等性は、以下のような問題を引き起こす可能性があります。

  1. バージョン管理システムへの不要な変更のコミット: gofmtを実行するたびにコメントのフォーマットが変わると、コード自体に論理的な変更がなくても、Gitなどのバージョン管理システムに差分が発生し、コミット履歴がノイズで埋め尽くされてしまいます。これは、実際のコード変更を追跡することを困難にします。
  2. CI/CDパイプラインの不安定化: 自動ビルドやテストを行うCI/CDパイプラインにおいて、gofmtの実行結果が非冪等であると、フォーマットチェックが不安定になり、不必要な失敗を引き起こす可能性があります。
  3. 開発者の混乱: 開発者が手動でgofmtを実行する際、毎回異なる結果が表示されると、どの状態が「正しい」フォーマットなのかが不明瞭になり、混乱を招きます。

Issue #1835は、まさにこのコメントフォーマットの非冪等性に関するものでした。このコミットは、この問題を解決し、gofmtの信頼性と使いやすさを向上させることを目的としています。

前提知識の解説

冪等性 (Idempotency)

冪等性とは、ある操作を複数回実行しても、その結果が初回実行時と同じになる性質を指します。数学では、f(f(x)) = f(x) のように表現されます。ソフトウェア開発においては、特に以下のような文脈で重要になります。

  • API設計: RESTful APIなどで、同じリクエストを複数回送信しても、リソースの状態が意図せず変化しないように設計されることがあります(例: PUTリクエスト)。
  • データベース操作: トランザクション処理において、冪等な操作は再試行が安全に行えることを保証します。
  • 設定管理: 設定ツールやスクリプトが、システムの状態を目的の状態に収束させる際に、何度実行しても同じ結果になるように設計されます。
  • コードフォーマッタ: gofmtのようなコードフォーマッタにおいて、冪等性は非常に重要です。同じコードに対してフォーマッタを複数回適用しても、出力されるコードが常に同じであるべきです。これにより、バージョン管理システムへの不要な変更のコミットを防ぎ、CI/CDパイプラインの安定性を保ちます。

go/printerパッケージ

go/printerパッケージは、Go言語の抽象構文木(AST: Abstract Syntax Tree)をGoのソースコードとして出力するためのパッケージです。gofmtツールは、このgo/printerパッケージを利用して、Goのソースコードを標準的なスタイルにフォーマットしています。

go/printerは、単にASTを文字列に変換するだけでなく、Goのコーディング規約(Go Proverbs)に基づいた整形ルールを適用します。これには、インデント、スペース、改行の調整、そしてコメントの配置と整形が含まれます。

抽象構文木 (AST: Abstract Syntax Tree)

ASTは、プログラミング言語のソースコードを抽象的な構文構造で表現したツリー構造のデータ表現です。コンパイラやインタプリタは、ソースコードを解析(パース)してASTを生成し、そのASTを基にコードの分析、最適化、コード生成などを行います。

Go言語の標準ライブラリには、go/parserパッケージがあり、GoのソースコードをパースしてASTを生成します。また、go/astパッケージは、ASTのノード構造を定義しています。go/printerは、このgo/astで定義されたASTを受け取り、整形されたソースコードとして出力します。

コメントは、ASTの一部として扱われることがありますが、その位置や整形はコードの構造とは独立している部分も多く、特に/* ... */形式のブロックコメントの整形は複雑になりがちです。

技術的詳細

このコミットの技術的詳細は、主にgo/printerパッケージ内のコメント処理ロジックと、テストフレームワークの改善にあります。

src/pkg/go/printer/printer.goの変更点

  1. stripCommonPrefix関数の修正:

    • この関数は、/*-styleのブロックコメントの各行から共通のプレフィックス(通常はインデントやアスタリスクなど)を取り除くために使用されます。これにより、コメントの内容が再整形後に適切にレイアウトされるようにします。
    • 変更前はlen(lines) < 2の場合に早期リターンしていましたが、変更後はlen(lines) <= 1に修正されています。これは、単一行のコメントの場合も処理をスキップするためです。
    • コメント内の空行の処理も修正され、lines[1+i] = ""からlines[1+i] = ""に変更されています。これは、rangeの開始インデックスに関するコメントの修正です。
    • 最も重要な変更は、共通プレフィックスの削除ロジックです。変更前はfor i, line := range lines[1:]で2行目以降を処理していましたが、変更後はfor i, line := range linesで全行を処理し、if i > 0 && line != ""という条件で2行目以降の非空行のみにプレフィックス削除を適用するように変更されています。これにより、より堅牢なプレフィックス削除が可能になります。
  2. writeComment関数における冪等性対応:

    • writeComment関数は、ASTからコメントを読み取り、整形して出力する役割を担っています。
    • このコミットで追加された重要なロジックは、/*-styleコメントが元のソースコードで1列目から始まっており、かつgo/printerがそのコメントをインデントする必要がある場合に適用されます。
    • 具体的には、if pos.IsValid() && pos.Column == 1 && p.indent > 0という条件が追加されました。
      • pos.IsValid(): コメントの位置情報が有効であるか。
      • pos.Column == 1: コメントがソースコードの1列目から始まっているか。
      • p.indent > 0: go/printerが現在、コメントをインデントする必要があるか(つまり、コメントがコードブロック内にあり、そのコードブロックがインデントされている場合など)。
    • この条件が真の場合、コメントの2行目以降の各行に3つのスペース(" ")をプレフィックスとして追加します。
    • この処理の目的は、Issue #1835で報告された問題に対処することです。元のコメントが1列目から始まっている場合、go/printerがインデントを適用すると、その後のstripCommonPrefix関数が共通プレフィックスを誤って計算し、非冪等な結果を生み出す可能性がありました。この修正により、go/printerがコメントをインデントする前に、あたかもコメントが最初からインデントされていたかのように見せかけることで、stripCommonPrefixの計算が安定し、複数回フォーマットしても同じ結果が得られるようになります。

src/pkg/go/printer/printer_test.goの変更点

  1. テストフレームワークのリファクタリング:

    • runcheck関数が大幅に簡素化され、formatという新しいヘルパー関数が導入されました。
    • format関数は、ソースコードをパースし、go/printerでフォーマットし、その結果が構文的に正しいことを検証する一連の処理をカプセル化しています。これにより、テストコードの重複が減り、可読性が向上しています。
    • lineAtdiffという新しいヘルパー関数が追加されました。
      • lineAt: 指定されたオフセットから始まる行を抽出します。
      • diff: 2つのバイトスライス(通常はフォーマット前とフォーマット後のコード)を比較し、差分があればエラーを返します。これにより、テストにおける期待値と実際の結果の比較がより詳細かつ明確に行えるようになりました。
  2. idempotentチェックモードの導入:

    • checkModeというビットフラグ列挙型にidempotentという新しいフラグが追加されました。
    • runcheck関数内で、idempotentモードが有効な場合、フォーマットされたゴールデンファイル(期待される出力)を再度format関数で処理し、その結果が元のゴールデンファイルと一致するかどうかをdiff関数で検証します。
    • これにより、特定のテストケースがコメントフォーマットの冪等性を満たしているかどうかを自動的にチェックできるようになりました。コミットメッセージにもあるように、すべてのファイルで冪等性を達成することは困難であるため、このフラグは冪等性が期待される特定のテストケースにのみ適用されます。
  3. 新しいテストケースの追加:

    • testdata/comments2.inputtestdata/comments2.goldenという新しいテストファイルが追加されました。これらは、Issue #1835で報告された具体的なシナリオを再現し、コメントフォーマットの冪等性を検証するために設計されています。
    • 既存のテストケース(empty.input, linebreaks.input, expressions.input, slow.input)にもidempotentフラグが適用され、これらのケースでも冪等性がチェックされるようになりました。

これらの変更により、go/printerはコメントのフォーマットにおいてより予測可能で安定した動作をするようになり、gofmtの信頼性が向上しました。

コアとなるコードの変更箇所

src/pkg/go/printer/printer.go

--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -449,11 +449,17 @@ func commonPrefix(a, b string) string {
 	return a[0:i]
 }
 
+// stripCommonPrefix removes a common prefix from /*-style comment lines (unless no
+// comment line is indented, all but the first line have some form of space prefix).
+// The prefix is computed using heuristics such that is is likely that the comment
+// contents are nicely laid out after re-printing each line using the printer's
+// current indentation.
+//
 func stripCommonPrefix(lines []string) {
-\tif len(lines) < 2 {\n+\tif len(lines) <= 1 {\n \t\treturn // at most one line - nothing to do
 \t}\n-\t// len(lines) >= 2\n+\t// len(lines) > 1
 \n \t// The heuristic in this function tries to handle a few
 \t// common patterns of /*-style comments: Comments where
 @@ -479,7 +485,7 @@ func stripCommonPrefix(lines []string) {
 \t\tfor i, line := range lines[1 : len(lines)-1] {\n \t\t\tswitch {\n \t\t\tcase isBlank(line):\n-\t\t\t\tlines[1+i] = "" // range starts at line 1\n+\t\t\t\tlines[1+i] = "" // range starts with lines[1]\n \t\t\tcase first:\n \t\t\t\tprefix = commonPrefix(line, line)\n \t\t\t\tfirst = false
 @@ -570,9 +576,9 @@ func stripCommonPrefix(lines []string) {
 	}\n 
 	// Remove the common prefix from all but the first and empty lines.
-\tfor i, line := range lines[1:] {\n-\t\tif len(line) != 0 {\n-\t\t\tlines[1+i] = line[len(prefix):] // range starts at line 1\n+\tfor i, line := range lines {\n+\t\tif i > 0 && line != "" {\n+\t\t\tlines[i] = line[len(prefix):]\n \t\t}\n \t}\n }\n@@ -612,6 +618,19 @@ func (p *printer) writeComment(comment *ast.Comment) {
 	// for /*-style comments, print line by line and let the
 	// write function take care of the proper indentation
 	lines := split(text)
+\n+\t// The comment started in the first column but is going
+\t// to be indented. For an idempotent result, add indentation
+\t// to all lines such that they look like they were indented
+\t// before - this will make sure the common prefix computation
+\t// is the same independent of how many times formatting is
+\t// applied (was issue 1835).
+\tif pos.IsValid() && pos.Column == 1 && p.indent > 0 {
+\t\tfor i, line := range lines[1:] {
+\t\t\tlines[1+i] = "   " + line
+\t\t}\n+\t}\n+\n \tstripCommonPrefix(lines)
 \n \t// write comment lines, separated by formfeed,
 @@ -1140,7 +1159,7 @@ func (p *trimmer) Write(data []byte) (n int, err error) {
 // ----------------------------------------------------------------------------
 // Public interface
 
-// A Mode value is a set of flags (or 0). They coontrol printing. \n+// A Mode value is a set of flags (or 0). They control printing. \n type Mode uint
 
 const (

src/pkg/go/printer/printer_test.go

--- a/src/pkg/go/printer/printer_test.go
+++ b/src/pkg/go/printer/printer_test.go
@@ -6,7 +6,9 @@ package printer
 
 import (
 	"bytes"
+\t"errors"
 	"flag"
+\t"fmt"
 	"go/ast"
 	"go/parser"
 	"go/token"
@@ -25,33 +27,28 @@ var update = flag.Bool("update", false, "update golden files")
 
 var fset = token.NewFileSet()
 
-func lineString(text []byte, i int) string {
-\ti0 := i
-\tfor i < len(text) && text[i] != '\n' {
-\t\ti++
-\t}
-\treturn string(text[i0:i])
-}
-\n type checkMode uint
 
 const (
 	export checkMode = 1 << iota
 	rawFormat
+\tidempotent
 )
 
-func runcheck(t *testing.T, source, golden string, mode checkMode) {
-\t// parse source
-\tprog, err := parser.ParseFile(fset, source, nil, parser.ParseComments)
+// format parses src, prints the corresponding AST, verifies the resulting
+// src is syntactically correct, and returns the resulting src or an error
+// if any.
+func format(src []byte, mode checkMode) ([]byte, error) {
+\t// parse src
+\tf, err := parser.ParseFile(fset, "", src, parser.ParseComments)
 	if err != nil {
-\t\tt.Error(err)
-\t\treturn
+\t\treturn nil, fmt.Errorf("parse: %s\n%s", err, src)
 	}
 
 	// filter exports if necessary
 	if mode&export != 0 {
-\t\tast.FileExports(prog) // ignore result
-\t\tprog.Comments = nil   // don't print comments that are not in AST
+\t\tast.FileExports(f) // ignore result
+\t\tf.Comments = nil   // don't print comments that are not in AST
 	}
 
 	// determine printer configuration
@@ -60,17 +57,72 @@ func runcheck(t *testing.T, source, golden string, mode checkMode) {
 	\tcfg.Mode |= RawFormat
 	}\n 
-\t// format source
+\t// print AST
 	var buf bytes.Buffer
-\tif err := cfg.Fprint(&buf, fset, prog); err != nil {\n-\t\tt.Error(err)
+\tif err := cfg.Fprint(&buf, fset, f); err != nil {
+\t\treturn nil, fmt.Errorf("print: %s", err)
 	}\n-\tres := buf.Bytes()\n \n-\t// formatted source must be valid
+\t// make sure formated output is syntactically correct
+\tres := buf.Bytes()
 \tif _, err := parser.ParseFile(fset, "", res, 0); err != nil {
+\t\treturn nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes())
+\t}\n+\n+\treturn res, nil
+}\n+\n+// lineAt returns the line in text starting at offset offs.
+func lineAt(text []byte, offs int) []byte {
+\ti := offs
+\tfor i < len(text) && text[i] != '\n' {
+\t\ti++
+\t}\n+\treturn text[offs:i]
+}\n+\n+// diff compares a and b.
+func diff(aname, bname string, a, b []byte) error {
+\tvar buf bytes.Buffer // holding long error message
+\n+\t// compare lengths
+\tif len(a) != len(b) {
+\t\tfmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b))
+\t}\n+\n+\t// compare contents
+\tline := 1
+\toffs := 1
+\tfor i := 0; i < len(a) && i < len(b); i++ {
+\t\tch := a[i]
+\t\tif ch != b[i] {
+\t\t\tfmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs))
+\t\t\tfmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs))
+\t\t\tfmt.Fprintf(&buf, "\n\n")
+\t\t\tbreak
+\t\t}\n+\t\tif ch == '\n' {
+\t\t\tline++
+\t\t\toffs = i + 1
+\t\t}\n+\t}\n+\n+\tif buf.Len() > 0 {
+\t\treturn errors.New(buf.String())
+\t}\n+\treturn nil
+}\n+\n+func runcheck(t *testing.T, source, golden string, mode checkMode) {
+\tsrc, err := ioutil.ReadFile(source)
+\tif err != nil {
+\t\tt.Error(err)
+\t\treturn
+\t}\n+\n+\tres, err := format(src, mode)
+\tif err != nil {
 \t\tt.Error(err)
-\t\tt.Logf("\n%s", res)
 \t\treturn
 	}\n \n@@ -89,23 +141,19 @@ func runcheck(t *testing.T, source, golden string, mode checkMode) {
 	\treturn
 	}\n \n-\t// compare lengths
-\tif len(res) != len(gld) {
-\t\tt.Errorf("len = %d, expected %d (= len(%s))", len(res), len(gld), golden)
+\t// formatted source and golden must be the same
+\tif err := diff(source, golden, res, gld); err != nil {
+\t\tt.Error(err)
+\t\treturn
 	}\n \n-\t// compare contents
-\tfor i, line, offs := 0, 1, 0; i < len(res) && i < len(gld); i++ {\n-\t\tch := res[i]\n-\t\tif ch != gld[i] {\n-\t\t\tt.Errorf("%s:%d:%d: %s", source, line, i-offs+1, lineString(res, offs))\n-\t\t\t\tt.Errorf("%s:%d:%d: %s", golden, line, i-offs+1, lineString(gld, offs))\n-\t\t\t\tt.Error()\n-\t\t\t\treturn
-\t\t}\n-\t\tif ch == '\n' {\n-\t\t\tline++
-\t\t\toffs = i + 1
+\tif mode&idempotent != 0 {
+\t\t// formatting golden must be idempotent
+\t\t// (This is very difficult to achieve in general and for now
+\t\t// it is only checked for files explicitly marked as such.)
+\t\tres, err = format(gld, mode)
+\t\tif err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil {
+\t\t\tt.Errorf("golden is not idempotent: %s", err)
 \t\t}\n 	}\n }\n@@ -142,15 +190,16 @@ type entry struct {
 
 // Use go test -update to create/update the respective golden files.
 var data = []entry{
-\t{"empty.input", "empty.golden", 0},\n+\t{"empty.input", "empty.golden", idempotent},\n \t{"comments.input", "comments.golden", 0},\n \t{"comments.input", "comments.x", export},\n-\t{"linebreaks.input", "linebreaks.golden", 0},\n-\t{"expressions.input", "expressions.golden", 0},\n-\t{"expressions.input", "expressions.raw", rawFormat},\n+\t{"comments2.input", "comments2.golden", idempotent},\n+\t{"linebreaks.input", "linebreaks.golden", idempotent},\n+\t{"expressions.input", "expressions.golden", idempotent},\n+\t{"expressions.input", "expressions.raw", rawFormat | idempotent},\n \t{"declarations.input", "declarations.golden", 0},\n \t{"statements.input", "statements.golden", 0},\n-\t{"slow.input", "slow.golden", 0},\n+\t{"slow.input", "slow.golden", idempotent},\n }\n \n func TestFiles(t *testing.T) {
@@ -248,7 +297,7 @@ func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) {
 	}\n }\n \n-// Verify that the printer produces always produces a correct program\n+// Verify that the printer produces a correct program\n // even if the position information of comments introducing newlines
 // is incorrect.\n func TestBadComments(t *testing.T) {
@@ -421,21 +470,8 @@ func TestX(t *testing.T) {
  package p
  func _() {}
  `
-\t// parse original
-\tf, err := parser.ParseFile(fset, "src", src, parser.ParseComments)
+\t_, err := format([]byte(src), 0)
 \tif err != nil {
-\t\tt.Fatal(err)
-\t}\n-\n-\t// pretty-print original
-\tvar buf bytes.Buffer
-\tif err = (&Config{Mode: UseSpaces, Tabwidth: 8}).Fprint(&buf, fset, f); err != nil {
-\t\tt.Fatal(err)
-\t}\n-\n-\t// parse pretty printed original
-\tif _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil {
-\t\tt.Fatalf("%s\n%s", err, buf.Bytes())
+\t\tt.Error(err)
 	}\n-\n }\n```

## コアとなるコードの解説

### `src/pkg/go/printer/printer.go`

*   **`stripCommonPrefix`関数の変更**:
    *   `if len(lines) <= 1`への変更は、コメントが1行以下の場合に処理をスキップすることで、不要な処理を避け、ロジックをより明確にしています。
    *   `for i, line := range lines`と`if i > 0 && line != ""`の組み合わせは、コメントの2行目以降の非空行に対してのみ共通プレフィックスの削除を適用するという意図をより正確に表現しています。これにより、コメントの整形がより堅牢になります。
*   **`writeComment`関数内の冪等性ロジック**:
    *   この変更は、`/*-style`コメントがコードの先頭(1列目)から始まり、かつ`go/printer`がそのコメントをインデントする必要がある場合に、コメントの2行目以降に3つのスペースを追加するというものです。
    *   これは、`gofmt`がコメントを整形する際に、元のコメントの開始位置と、整形後のインデントレベルの間に不整合が生じることで発生する非冪等性の問題を解決します。
    *   具体的には、`pos.Column == 1`はコメントがファイルの先頭行から始まることを示し、`p.indent > 0`はプリンタがそのコメントをインデントしようとしていることを示します。この両方の条件が満たされる場合、コメントの2行目以降に手動でインデントを追加することで、`stripCommonPrefix`が共通プレフィックスを正しく認識し、複数回のフォーマットで同じ結果が得られるようにします。これは、Issue #1835で報告された具体的なバグに対する修正です。

### `src/pkg/go/printer/printer_test.go`

*   **テストフレームワークのリファクタリング**:
    *   `format`関数の導入により、テストコードが大幅に簡素化され、Goのソースコードのパース、プリント、再パースという一連の処理が再利用可能な形で提供されます。これにより、テストの記述が容易になり、エラーハンドリングも一元化されます。
    *   `diff`関数の追加は、テストにおける期待値と実際の結果の比較をより詳細に行うためのものです。バイトスライスレベルでの差分を正確に報告することで、テストのデバッグが容易になります。
*   **`idempotent`チェックモード**:
    *   この新しいモードは、`go/printer`の出力が冪等であるかどうかを検証するためのものです。特定のテストケースに対してこのフラグを設定することで、`gofmt`を複数回適用しても結果が変わらないことを保証します。
    *   `if mode&idempotent != 0`のブロック内で、ゴールデンファイル(期待されるフォーマット済みコード)を再度`format`関数で処理し、その結果が元のゴールデンファイルと一致するかどうかを`diff`で確認します。これにより、冪等性が破られていないかを自動的に検出できます。
*   **テストケースの更新**:
    *   `comments2.input`と`comments2.golden`の追加は、Issue #1835の具体的なシナリオをカバーするためのものです。これらのテストケースは、コメントのインデントと共通プレフィックスの処理が冪等であることを確認します。
    *   既存のテストケースに`idempotent`フラグが追加されたことで、より広範なシナリオで冪等性が検証されるようになりました。

これらの変更は、`go/printer`の堅牢性と信頼性を高め、`gofmt`がGo開発者にとってより使いやすいツールとなることに貢献しています。

## 関連リンク

*   Go Issue #1835: [https://github.com/golang/go/issues/1835](https://github.com/golang/go/issues/1835)
*   Go `go/printer`パッケージのドキュメント: [https://pkg.go.dev/go/printer](https://pkg.go.dev/go/printer)
*   Go `gofmt`コマンドのドキュメント: [https://pkg.go.dev/cmd/gofmt](https://pkg.go.dev/cmd/gofmt)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント
*   GitHubのGoリポジトリのIssueトラッカー
*   Go言語のソースコード
*   冪等性に関する一般的なプログラミングの概念