🛠️

Docker(moby/moby)リポジトリにおけるGo言語の変数名頻度分析

2024/10/04に公開

はじめに

Dockerはコンテナ技術のデファクトスタンダードとなっており、そのコアとなるmoby/mobyリポジトリは多くの開発者にとって学習の宝庫です。今回は、このリポジトリ内で使用されているGo言語のグローバル変数名とローカル変数名を分析し、頻出する変数名をリストアップしました。その結果から、コードのスタイルや開発者の習慣について考察してみたいと思います。

変数名の出現頻度ランキング

以下がmoby/mobyリポジトリ内で最も多く使用されている変数名とその出現回数です。

err: 50789
_: 19968
ok: 7589
i: 6629
n: 5788
b: 5598
shift: 4563
e1: 4479
_p0: 3935
v: 3904
l: 3769
s: 3262
out: 3212
iNdEx: 3210

変数名の分析と考察

err (50789回)

errは圧倒的な出現回数を誇ります。Go言語では、エラー処理が非常に重要であり、関数の戻り値としてerror型を返すことが一般的です。エラーチェックのためのerr変数が多用されるのは当然と言えるでしょう。

使用例

c, err := newController(ctx, reqHandler, opt)
if err != nil {
    return nil, err
}

このコードでは、newControllerから返されるエラーをerrに格納し、もしエラーが発生した場合はすぐにnilerrを返すことで処理を中断します。errという短縮形の変数名は、Goの標準的なエラーハンドリングパターンに従っており、エラーチェックを簡潔に行うことを目的としています。

このように、出現回数と使用例を組み合わせることで、errがGo言語においてどれほど重要で多用されているかを示しています。

_ (19968回)

アンダースコア(_)はGo言語の「ブランク識別子」として、変数を無視する際に使用されます。これだけ多く使用されているのは、不要な値を明示的に無視するGoのコーディングスタイルを反映しています。またブランク識別子はメモリを確保しないため、無駄なリソース消費を避けるためにも有用です。

使用例

for _, r := range duResp.Record {
    items = append(items, &types.BuildCache{
        ID:          r.ID,
        Parent:      r.Parent, //nolint:staticcheck // ignore SA1019 (Parent field is deprecated)
        Parents:     r.Parents,
        Type:        r.RecordType,
        Description: r.Description,
        InUse:       r.InUse,
        Shared:      r.Shared,
        Size:        r.Size_,
        CreatedAt:   r.CreatedAt,
        LastUsedAt:  r.LastUsedAt,
        UsageCount:  int(r.UsageCount),
    })
}

このコードのfor _, r := range duResp.Recordにおけるアンダースコアは、Goの「ブランク識別子」として使用されています。Go言語では、戻り値やループ変数の一部が不要な場合に、それを無視するためにアンダースコアを使います。具体的には、このforループではrange構文によってduResp.Recordの各要素を反復処理していますが、最初の戻り値であるインデックスは必要ないため、無視されています。

ok (7589回)

ok は、マップから値を取得する際や、型アサーションの結果を受け取る際に使われるブール値です。例えば、value, ok := myMap[key]のように使用されます。この変数名もGoにおける一般的な慣習を示しています。

使用例

if _, ok := ref.(reference.Digested); ok {
    return nil, errors.New("build tag cannot contain a digest")
}

このコードでは、okは型アサーションの結果を受け取るために使用されています。ref.(reference.Digested)で、refreference.Digested型にアサーションできるかを確認しており、アサーションが成功した場合にokはtrueとなります。もしokがtrueならば、refreference.Digested型であることが確定し、そこでエラー処理が行われます。このように、型アサーションの結果を処理する際にokという名前の変数を使うのは、Go言語の一般的な慣習です。

i (6629回)

iは、ループカウンタとして非常によく使用される変数名です。短く、ループのインデックスを表現するための標準的な命名として使われることが多く、Go言語でも慣習的に使われます。

使用例

for i, p := range wo.Platforms {
    wo.Platforms[i] = platforms.Normalize(p)
}

このコードでは、iはforループのカウンタとして使われ、インデックスを用いてwo.Platformsの要素を特定し、正規化してその要素へ再代入しています。このようなループ処理では、iは簡潔かつ分かりやすい変数名として標準的に使用されます。

n (5788回)

nは、一時的な数値を表すために使われる短い変数名です。特にforループ内での使用が多いと考えられます。nという短い変数名は、直感的かつシンプルなため、Goのコードでは頻繁に登場します。

使用例

func (e *logEntryEncoder) Encode(l *LogEntry) error {
    n := l.Size()

    total := n + binaryEncodeLen
    if total > len(e.buf) {
        e.buf = make([]byte, total)
    }
    binary.BigEndian.PutUint32(e.buf, uint32(n))

    if _, err := l.MarshalTo(e.buf[binaryEncodeLen:]); err != nil {
        return err
    }
    _, err := e.w.Write(e.buf[:total])
    return err
}

このコードでは、nLogEntry構造体のサイズを表すために使われています。l.Size()メソッドを呼び出してnにサイズを代入し、その後にバッファのサイズを調整するために利用されています。nは一時的な数値として格納され、次の処理に使用されます。このように、nはシンプルな数値を格納するための変数名として広く使われます。

b (5598回)

bは、構造体のインスタンスを表すためにしばしば使用される変数名です。特に、BuilderBufferなどの構造体の頭文字として使われ、シンプルかつ直感的な命名が求められる場面で用いられます。bは短い変数名であるため、構造体のメソッド呼び出し時に使用されることが多く、コードの可読性を保ちながら簡潔に記述できます。

使用例

b := &Builder{
    controller:     c,
    dnsconfig:      opt.DNSConfig,
    reqBodyHandler: reqHandler,
    jobs:           map[string]*buildJob{},
    useSnapshotter: opt.UseSnapshotter,
}

func (b *Builder) Close() error {
    return b.controller.Close()
}

func (b *Builder) RegisterGRPC(s *grpc.Server) {
    b.controller.Register(s)
}

func (b *Builder) Cancel(ctx context.Context, id string) error {
    b.mu.Lock()
    if j, ok := b.jobs[id]; ok && j.cancel != nil {
        j.cancel()
    }
    b.mu.Unlock()
    return nil
}

このコードでは、bBuilder構造体のインスタンスを表しており、そのメソッドを呼び出す際に使用されています。bという短い変数名は、Builderのような長い名前の構造体に対してシンプルで読みやすい形でアクセスできるようにするためのもので、メソッド呼び出しの中で効果的に使われています。

  • 使用意図:
    bは、Builder構造体を操作するためのインスタンス名として使用されており、メソッド呼び出し時にb.Close()b.RegisterGRPC()のように利用されます。短く簡潔な名前を用いることで、コードをよりシンプルに保ちながら、構造体の操作をわかりやすく表現できます。このパターンは、Go言語で構造体やオブジェクトを操作する際に広く使われています。

まとめ:

bは、Builderのような構造体を扱う際に、そのインスタンスを示すための変数名として使用されます。構造体の頭文字を変数名にすることで、簡潔かつ読みやすいコードを維持し、メソッド呼び出しなどで効率的に操作が行えるようになります。Go言語では、構造体やインスタンスを操作する際に、bのような短い変数名が一般的に使われています。

この説明では、bBuilder構造体のインスタンスとして使用される目的と、その役割を具体的に説明しています。また、bのような短い変数名を使うことで、コードの可読性と簡潔さを保ちながら構造体を効率的に操作できる点を強調しています。

v (3904回)

vは、ループ内で値を表すためにしばしば使用される変数名です。特に、マップのキーと値を反復処理する際に、vという短い名前が使われることが一般的です。シンプルで直感的な命名を使うことで、コードの可読性を保ちながら効率的にループ処理が行われます。Go言語では、vのような簡潔な変数名が、ループ内での値の操作に広く用いられています。

使用例

for k, v := range opt.Options.BuildArgs {
    if v == nil {
        continue
    }
    frontendAttrs["build-arg:"+k] = *v
}

for k, v := range opt.Options.Labels {
    frontendAttrs["label:"+k] = v
}

このコードでは、vはマップの値を表しています。for k, v := range opt.Options.BuildArgsfor k, v := range opt.Options.Labelsのように、kでキーを、vで値を受け取ることで、マップの各要素を反復処理しています。vという短い変数名は、値を操作する際に簡潔に書けるため、Goの慣習的なコーディングスタイルに合っています。

  • 使用意図:
    vは、マップ内の値を表すために使われています。opt.Options.BuildArgsopt.Options.Labelsというマップの各エントリに対して、キーと値のペアを取得し、値vnilでない場合にfrontendAttrsに代入されるという流れです。短い変数名であるvを使うことで、値へのアクセスが明確かつ簡潔になります。これは、Go言語においてよく使われるマップ操作のパターンであり、コードの簡潔さを保ちながら直感的な操作を可能にしています。

まとめ:

vは、ループ内で値を表すために広く使用される変数名です。特に、マップのキーと値を扱う際に、簡潔でわかりやすい命名として使われます。Go言語のスタイルにおいて、vという短い変数名は、コードの可読性と効率を維持しながら、ループ内の値操作をスムーズに行うために役立っています。

l (3769回)

lは、一般的に「長さ(length)」を示すために使われる変数名で、データのバイト数や要素数などを扱う際に利用されます。特に、データのシリアライズやバッファ操作の際に、配列やスライスの長さを示すために使われることが多く、短くシンプルな命名規則が好まれるGo言語において、広く使用されています。

使用例

var l int
_ = l

このコードでは、lint型の変数として宣言され、データの長さやサイズを格納する目的で使われています。シリアライズやバッファ操作を行う処理において、データの長さを扱う際にlという短い変数名が使われ、簡潔に記述されています。この場合、データの正確な長さを追跡するために、変数lが一時的に使われています。

  • 使用意図:
    変数lは、データの長さやサイズを扱うために使われており、処理中に配列やスライスのサイズを計算したり操作する際に、シンプルな名前として使用されています。この例では、データのバイト数を表す場面でlが使用されることが想定され、コードの冗長さを避け、可読性を保つことが目的です。

コンテキスト: 自動生成されたコード

このコードは、protoc-gen-gogoによって自動生成されたものであり、.protoファイルを元に生成されています。lといった短い変数名は、このような自動生成コードにおいてもよく使用され、データ処理の中で効率的にサイズや長さを扱うための変数名として定着しています。

まとめ:

lは、データの長さやサイズを管理するために使われる変数名で、Go言語において短くシンプルな命名規則の一環として広く使用されています。このような短い変数名を使うことで、コードの可読性を損なわず、効率的なデータ処理が可能となります。

s (3262回)

sは、文字列やスライスなどを表すために短い変数名としてよく使用されます。特に、Go言語では文字列操作やスライス操作が頻繁に行われるため、sというシンプルな変数名が好まれます。Goのコーディングスタイルとして、明確で簡潔な変数名を用いることで、可読性と効率性を両立させることがよく見られます。

使用例

s := strings.ToLower(strings.TrimSpace(r.FormValue(k)))

このコードでは、変数sr.FormValue(k)で取得したフォームの値を一時的に格納し、それに対して文字列操作を行うために使用されています。具体的には、TrimSpaceで前後の空白を削除し、ToLowerで文字列を小文字に変換しています。このような文字列操作の中で、短く直感的な変数名sを使うことによって、簡潔で読みやすいコードが実現されています。

  • 使用意図:
    変数sは、HTTPリクエストから取得したフォームの値(文字列)を操作するために使われています。ここでのsは、余分な空白を除去し、小文字に変換されたフォームの入力値を格納し、後続の条件式でその値が空か、"0""no""false""none"であるかどうかを判断するために利用されています。短い変数名sを使うことで、処理内容を簡潔に表現し、コードの可読性が高められています。

まとめ:

sは、文字列を表す変数名として広く使用されます。特に、文字列操作やデータ処理の場面で短く簡潔な変数名として用いられ、コードのシンプルさと効率性を保つための一環として定着しています。Go言語のスタイルに従い、sのようなシンプルな変数名を使うことで、コードの明確さと可読性が向上しています。

shift (4563回)

shiftは、主にビット演算におけるシフト操作や、データ処理における位置の調整に使われます。ビットシフトは、データの効率的な操作や変換に欠かせないもので、特定のアルゴリズムやプロトコルで頻繁に使用されます。shiftがこれだけ多く使用されているのは、データのエンコードやデコードなどの処理において、位置の調整やバイト列の操作が繰り返されるからです。

使用例

for shift := uint(0); ; shift += 7 {
    if shift >= 64 {
        return ErrIntOverflowEntry
    }
    b := dAtA[iNdEx]
    wire |= uint64(b&0x7F) << shift
    if b < 0x80 {
        break
    }
}

このコードでは、shift変数がビットシフト演算のために使用されています。特に、Protocol Buffers形式のデータのデシリアライズにおいて、バイト列を読み込みながら7ビットずつシフトし、wireに値を蓄積しています。この操作は、データを効率的に取り扱うために非常に重要です。

  • 使用意図:
    shiftは、ビット演算を使ってデータのバイトを処理する際に利用されています。このコードは、Protocol Buffersのデータフォーマットに基づいており、可変長整数のデコードにおいて7ビットずつシフトしながらデータを読み込む典型的なパターンです。shiftを使用することで、複数のバイトにまたがるデータを効率的に集約し、適切に変換しています。

コンテキスト: 自動生成されたコード

このコードは、protoc-gen-gogoによって自動生成されたものであり、.protoファイルを元に生成されています。shiftのようなビット演算が頻繁に使用される背景には、プロトコルバッファのデータのエンコード・デコードの効率化が目的としてあります。自動生成コードにおいては、データの効率的な読み書きが求められるため、ビットシフトは重要な役割を果たします。

まとめ:

shiftはビット演算によるシフト操作を効率的に行うために使用され、特にデータのエンコードやデコード、位置の調整に欠かせない役割を果たします。このコードにおいては、7ビットずつデータをシフトして読み込み、変数に値を蓄積するプロセスに用いられ、データ処理の効率を最大限に引き出すために活用されています。

e1 (4479回)

e1は、複数のエラー処理やシステムコールの戻り値を扱う際に、一時的な変数として使われることが多いです。特に、システムコールや外部のプロセス呼び出しが関わる部分で、複数の戻り値の中からエラーチェックを行うために使用されることが考えられます。エラーが複数存在する場合や、同時に複数のシステム呼び出しがある場合に、e1などの簡潔な名前が使われることが一般的です。

使用例

r1, _, e1 := syscall.SyscallN(procLogonUserW.Addr(), uintptr(unsafe.Pointer(username)), uintptr(unsafe.Pointer(domain)), uintptr(unsafe.Pointer(password)), uintptr(logonType), uintptr(logonProvider), uintptr(unsafe.Pointer(token)))
if r1 == 0 {
    err = errnoErr(e1)
}

このコードでは、e1はシステムコールsyscall.SyscallNの結果として返されるエラーを格納するために使用されています。r1はシステムコールの戻り値で、e1はエラーコードとして取得されます。このパターンは、システムレベルのエラーチェックを行う際によく見られるものであり、e1という変数名を使うことで、複数の戻り値の中からエラーハンドリングを簡潔に行うことができます。

  • 使用意図:
    変数e1は、Windowsのシステムコールにおいてエラー情報を格納するために使われています。このコードでは、syscall.SyscallNからの戻り値がr1に格納され、もしその戻り値が0(失敗)であれば、e1に格納されたエラーを使ってエラーハンドリングを行います。Goの標準的なエラーチェックパターンに従っており、e1という名前はエラーチェックを簡潔にするために使用されています。

コンテキスト: 自動生成されたコード

このコードは、Windowsのシステムコールを扱うための自動生成コードであり、go generateコマンドを用いて生成されたものです。システムレベルのコードでは、エラーハンドリングが頻繁に行われるため、e1のような変数名が一般的に使用されます。このコードでは、Windows固有のログオン処理を扱っており、エラーハンドリングのためにe1が使用されています。

まとめ:

e1は、システムコールやエラー処理において、複数の戻り値の中からエラーを格納するために使われます。特に、システムレベルの呼び出しや複数のエラーチェックが必要な場面で、このような短い変数名を使うことでコードを簡潔に保ち、エラーハンドリングを効率的に行うことができます。

_p0 (3935回)

_p0は、自動生成コードにおいて、一時的なパラメータを表す変数名として使用されます。Go言語の自動生成されたシステムコール関連のコードでは、特定の引数やフラグを格納するためにこのような変数名が利用されます。特に、システムコールや低レベルの処理において、引数が複数存在する場合に、変数名を簡潔かつ一意にするためにアンダースコア付きの名前が使われることが多いです。

使用例

var _p0 uint32
if releaseAll {
    _p0 = 1
}
r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize)))

このコードでは、_p0はシステムコールsyscall.SyscallNに渡される引数の一部として使用されています。releaseAllフラグがtrueの場合に_p01に設定され、それがシステムコールの引数として使用されます。_p0は、関数内で一時的なフラグを保持するために使われており、このような変数名が自動生成コードで多用される理由は、複数のシステムコールの引数やフラグの役割を明示するためです。

  • 使用意図:
    _p0は、adjustTokenPrivileges関数内でシステムコールに渡される一時的なフラグを保持しています。releaseAlltrueである場合に、この変数に1が代入され、WindowsのシステムコールAdjustTokenPrivilegesの引数として使用されます。変数名_p0は自動生成されたコードでよく見られるパターンで、パラメータを簡潔に定義しつつもコードの可読性を損なわないように工夫されています。

コンテキスト: 自動生成されたコード

このコードは、go generateコマンドを用いて自動生成されており、Windowsのシステムコールを操作するための低レベルAPIです。自動生成コードでは、複数のパラメータを一時的に保持するために_p0のような変数が使われます。この場合、システムコールの引数として簡単に扱える形式にするために、変数名が自動的に付与されています。

まとめ:

_p0は、システムコールの引数や一時的なフラグを保持するために使われる変数名であり、自動生成されたコードで頻繁に使用されます。このような変数名は、パラメータの管理やコードの可読性を保ちながら、複数の引数を効率的に扱うために重要な役割を果たします。

iNdEx (3210回)

iNdExは、一般的にバイト配列のインデックスを示す変数名として使用され、データのデコードやシリアライズ処理で頻繁に見られます。この変数名は「index」を意味するもので、プロトコルバッファのようなデータ形式を扱う際に、バイトストリームを追跡するために使われます。特に自動生成されたコードでは、iNdExのような変数名がよく使用され、データの位置を管理します。

使用例

l := len(dAtA)
iNdEx := 0
for iNdEx < l {
    preIndex := iNdEx
    var wire uint64
    for shift := uint(0); ; shift += 7 {
        if shift >= 64 {
            return ErrIntOverflowEntry
        }
        if iNdEx >= l {
            return io.ErrUnexpectedEOF
        }
        b := dAtA[iNdEx]
        iNdEx++
        wire |= uint64(b&0x7F) << shift
        if b < 0x80 {
            break
        }
    }

このコードでは、iNdExはバイト配列dAtA内の現在のインデックスを追跡するために使われています。この変数は、バイトストリームを解析しながら、どの位置にいるかを管理します。特に、バイト単位でデータを処理し、プロトコルバッファなどの形式に従ってデコードする際に使用されます。

  • 使用意図:
    iNdExは、データのバイト位置を示すためのインデックスとして機能します。デコード処理の中で、バイトごとに位置を管理しながら、どの部分が現在処理中かを示す役割を果たします。このような変数名は自動生成されたコードでよく見られ、特にプロトコルバッファのような複雑なデータ形式で、バイトストリームのインデックス管理が重要になるため使用されています。

コンテキスト: 自動生成されたコード

このコードは、.protoファイルから自動生成されたもので、protoc-gen-gogoによって生成されています。バイト列を効率的にデコードするために、iNdExのような変数が使われ、バイト位置を細かく追跡しながらデータを解析します。自動生成されたコードでは、変数名が一貫して簡潔に保たれ、データの処理効率を高めるための工夫が見られます。

まとめ:

iNdExは、バイト配列やデータストリームのインデックスを管理するための変数名であり、特にデコード処理で重要な役割を果たします。Go言語の自動生成コードでは、バイトごとの処理を追跡し、効率的にデータを操作するために、このような変数名が頻繁に使われています。

out (3212回)

outは、出力結果や返り値を格納するための変数名として使われることが一般的です。特に、関数やメソッドから複数の値を返す際に、結果を一時的に保持するために使用されます。Go言語では、関数が複数の戻り値を返すことが一般的であり、その際にoutという変数名がよく使われます。

使用例

out, ref, err := i.ExporterInstance.Export(ctx, src, inlineCache, sessionID)
if err != nil {
    return out, ref, err
}

desc := ref.Descriptor()
imageID := out[exptypes.ExporterImageDigestKey]

このコードでは、outExporterInstance.Exportメソッドの結果を格納するために使用されています。このExportメソッドは、複数の戻り値(outreferr)を返し、そのうちoutは、文字列のマップでイメージの識別情報や出力結果を保持しています。out変数を使うことで、メソッドの出力を一時的に保持し、その後の処理で利用することができます。

  • 使用意図:
    outは、Exportメソッドからの出力を受け取り、結果を処理するために使用されます。この例では、outがイメージIDやその他の出力情報を保持し、それに基づいてさらなる処理が行われています。outという変数名は、結果を一時的に格納し、それを処理するための一般的なパターンを表しています。

まとめ:

outは、関数やメソッドからの出力結果を一時的に保持するための変数名として広く使用されます。この例では、Exportメソッドの結果を受け取り、それに基づいて処理を進めるためにoutが利用されています。Go言語において、こうした短い変数名は、可読性を保ちつつ、効率的な処理を実現するために役立っています。

コーディングスタイルへの影響

この分析から、Dockerのmoby/mobyリポジトリにおけるコーディングスタイルや慣習を読み取ることができます。

  • エラー処理の徹底: errの出現頻度が圧倒的であることから、moby/mobyプロジェクトではエラー処理が非常に重要視されていることがわかります。エラーを適切にキャッチし、処理を停止するためのコードが頻繁に記述されていることが明確です。

  • Goの慣習的スタイルの採用: アンダースコア_okの使用例からも、Goの標準的な慣習に従ったコーディングスタイルが採用されていることがわかります。特に、不要な戻り値を明示的に無視するための_や、マップの存在確認や型アサーションで使用されるokの頻繁な使用が見られます。

  • 短い変数名の多用: inのようなシンプルで短い変数名は、ループカウンタや一時的な値を保持するために頻繁に使用されており、Goのコードでの効率的な記述が意識されています。bvといった短い変数名も、構造体やマップ操作で利用され、コードの簡潔さが保たれています。

  • 自動生成されたコードの存在: iNdEx_p0shiftなどの変数は、自動生成されたコードやシステムコールに関連する処理で多用されていることから、プロジェクトには自動生成されたコードが含まれていることが推測されます。

まとめ

moby/mobyリポジトリにおける変数名の出現頻度を分析することで、エラー処理の徹底やGo言語の標準的なコーディングスタイルの採用、効率的な変数名の選定がプロジェクト全体に浸透していることがわかりました。特に、errok、アンダースコア_の使用は、エラーハンドリングや不要な戻り値を無視するGoのイディオムがプロジェクト内で広く採用されていることを示しています。また、自動生成されたコードの存在も見受けられ、プロジェクト全体で自動化された部分が重要な役割を果たしていることがわかります。

このような分析を通じて、Go言語の慣習を学び、プロジェクトのコーディングスタイルを理解することは、他のプロジェクトや自身の開発にも有益な視点を提供してくれるでしょう。Dockerのmoby/mobyリポジトリは、Go言語のベストプラクティスを学ぶための良い教材と言えます。

今回の分析に使用したプログラム

go run ./main.go ../moby | sort -k2,2nr のように実行することで、変数とその出現回数を降順で出力することができます。

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

// 変数名の出現回数を格納するマップ
var vars = make(map[string]int)

// 指定したディレクトリ内の Go ファイルを再帰的に検索してパースする
func parseGoFilesInDir(dir string) error {
	// ディレクトリを走査
	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Go ファイルのみを対象とする
		if !info.IsDir() && strings.HasSuffix(path, ".go") {
			if err := parseFile(path); err != nil {
				fmt.Printf("Error parsing %s: %v\n", path, err)
			}
		}
		return nil
	})
}

// Go ファイルをパースし、変数名を抽出する
func parseFile(filename string) error {
	// ソースコードを解析するための token セットを作成
	fs := token.NewFileSet()

	// Go ファイルをパース
	node, err := parser.ParseFile(fs, filename, nil, parser.AllErrors)
	if err != nil {
		return err
	}

	// AST をトラバースして変数名を見つける
	ast.Inspect(node, func(n ast.Node) bool {
		// 変数宣言(ast.AssignStmt)をチェック
		if decl, ok := n.(*ast.AssignStmt); ok {
			for _, expr := range decl.Lhs {
				// 左辺の識別子を取得
				if ident, ok := expr.(*ast.Ident); ok {
					vars[ident.Name]++
				}
			}
		}

		// 変数定義(ast.ValueSpec)をチェック
		if decl, ok := n.(*ast.ValueSpec); ok {
			for _, ident := range decl.Names {
				vars[ident.Name]++
			}
		}

		return true
	})

	return nil
}

func main() {
	// コマンドライン引数を確認(ディレクトリパス)
	if len(os.Args) < 2 {
		fmt.Println("Usage: go run main.go <directory_path>")
		return
	}

	// 引数からディレクトリパスを取得
	dir := os.Args[1]

	// Go ファイルをパースして変数名をカウント
	if err := parseGoFilesInDir(dir); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// 変数名とその出現回数を表示
	for k, v := range vars {
		fmt.Println(k, v)
	}

}


Discussion