🛠️

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のような長い名前の構造体に対してシンプルで読みやすい形でアクセスできるようにするためのもので、メソッド呼び出しの中で効果的に使われています。

v (3904回)

vは、ループ内で値(value)を表すためにしばしば使用される変数名です。特にマップのキーと値を反復処理する際に、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の慣習的なコーディングスタイルに合っています。

l (3769回)

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

使用例

var l int
_ = l

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

s (3262回)

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

使用例

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

このコードでは、変数sr.FormValue(k)で取得したフォームの値を一時的に格納し、それに対して文字列操作を行うために使用されています。具体的には、TrimSpaceで前後の空白を削除し、ToLowerで文字列を小文字に変換しています。このような文字列操作の中で、短く直感的な変数名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に値を蓄積しています。この操作は、データを効率的に取り扱うために非常に重要です。

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という変数名を使うことで、複数の戻り値の中からエラーハンドリングを簡潔に行うことができます。

_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は、関数内で一時的なフラグを保持するために使われており、このような変数名が自動生成コードで多用される理由は、複数のシステムコールの引数やフラグの役割を明示するためです。

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

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変数を使うことで、メソッドの出力を一時的に保持し、その後の処理で利用することができます。

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

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

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

  • 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