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

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

このコミットは、Windows環境におけるGo言語のosパッケージのコンソールI/O処理を改善するものです。具体的には、syscall.WriteConsole関数が大きなバッファを扱う際に失敗する問題を解決するため、書き込み処理を小さなチャンクに分割して実行するように変更しています。これにより、コンソールへの大量出力時の安定性が向上します。

コミット

commit 28cb9fd5096b3714351cb0312dda37816b1d7d8d
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Sep 19 16:55:21 2012 +1000

    os: use small writes during console io
    
    Fixes #3767
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6523043

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

https://github.com/golang/go/commit/28cb9fd5096b3714351cb0312dda37816b1d7d8d

元コミット内容

Go言語のosパッケージにおいて、Windows環境でのコンソールI/O時に小さな書き込みを使用するように変更。これにより、Issue #3767で報告された問題が修正されます。

変更の背景

この変更は、Go言語のIssue #3767「os.Stdout.Write fails on Windows when writing large amounts of data」を修正するために行われました。この問題は、WindowsのコンソールAPIであるWriteConsole関数が、一度に大量のデータを書き込もうとすると失敗するというものでした。特に、数万バイトを超えるような大きなバッファをWriteConsoleに渡すと、エラーが発生し、Goプログラムがコンソールに正しく出力できなくなるというバグがありました。

この問題は、Goプログラムが標準出力や標準エラー出力に大量のログやデータを書き出す際に、アプリケーションのクラッシュや予期せぬ動作を引き起こす可能性がありました。そのため、Windows環境でのGoプログラムの安定性と信頼性を向上させるために、この修正が必要とされました。

前提知識の解説

WindowsコンソールI/OとWriteConsole

Windowsオペレーティングシステムでは、コンソール(コマンドプロンプトやPowerShellなどのテキストベースのインターフェース)への入出力は、Win32 APIを通じて行われます。テキストの出力には主にWriteConsole関数が使用されます。

  • WriteConsole関数: この関数は、指定された文字データをコンソールスクリーンバッファに書き込みます。通常、HANDLE(コンソール出力バッファのハンドル)、書き込む文字データへのポインタ、書き込む文字数、実際に書き込まれた文字数を受け取ります。
  • UTF-16エンコーディング: WindowsのネイティブAPIの多くは、テキストデータをUTF-16エンコーディングで扱います。Go言語の文字列はUTF-8でエンコードされているため、WriteConsoleに渡す前にUTF-16に変換する必要があります。utf16.Encode関数は、Goのruneスライス(Unicodeコードポイント)をUTF-16のuint16スライスに変換するために使用されます。

syscallパッケージ

Go言語のsyscallパッケージは、オペレーティングシステムのプリミティブな関数(システムコール)への低レベルなインターフェースを提供します。これにより、Goプログラムから直接OSのAPIを呼び出すことが可能になります。syscall.WriteConsoleは、WindowsのWriteConsole APIをGoから呼び出すためのラッパーです。

コンソールI/Oのバッファリング

一般的に、I/O操作はパフォーマンス向上のためにバッファリングされます。しかし、WriteConsoleのような低レベルAPIは、OSカーネルとの直接的なやり取りを伴うため、特定の条件下ではバッファサイズに制限がある場合があります。このコミットの背景にある問題は、まさにこのWriteConsoleの内部的なバッファ処理、またはOSレベルでの制限に起因していると考えられます。

技術的詳細

このコミットの核心は、WindowsのWriteConsole APIが一度に処理できるデータ量に暗黙の制限があるという問題への対処です。Goのosパッケージ内のFile.writeConsoleメソッドは、コンソールへの書き込みを担当しています。元の実装では、Goのバイトスライスをruneスライスに変換し、さらにそれをUTF-16のuint16スライスにエンコードした後、syscall.WriteConsoleに直接渡していました。

しかし、このsyscall.WriteConsoleが「大きなバッファを与えられると失敗する」という挙動が確認されました。コミットメッセージには「16000文字にバッファを制限する。この数値はsyscall.WriteConsoleの実験によって発見された」と明記されており、これは経験的に見つけ出された安全な最大書き込みサイズであることを示しています。

この問題を解決するため、File.writeConsoleメソッドは以下のように変更されました。

  1. maxWrite定数の導入: const maxWrite = 16000という定数が導入されました。これは、一度にWriteConsoleに渡すUTF-16文字の最大数を示します。
  2. チャンク処理: len(runes) > 0のループ内で、元のruneスライスをmaxWriteで定義されたサイズ以下の小さなチャンクに分割して処理するようになりました。
    • m := len(runes)で現在のrunesスライスの長さを取得します。
    • if m > maxWrite { m = maxWrite }で、チャンクサイズがmaxWriteを超えないように制限します。
    • chunk := runes[:m]で、現在のチャンクを抽出します。
    • runes = runes[m:]で、処理済みの部分を元のスライスから削除し、次のループで残りの部分を処理するようにします。
  3. UTF-16エンコードと書き込み: 各チャンクはutf16.Encode(chunk)によってUTF-16にエンコードされ、その後、syscall.WriteConsoleに渡されます。この内部ループは、UTF-16エンコードされたデータがさらにuint16sスライスとして残っている限り続きます。

この変更により、Goプログラムがコンソールに大量のデータを書き込もうとした場合でも、内部的には小さなチャンクに分割されてWriteConsoleに渡されるため、APIの制限に引っかかることなく、安定して出力が完了するようになります。

また、この変更を検証するために、os_test.goTestLargeWriteToConsoleという新しいテストが追加されました。このテストは、StdoutStderrに32000バイトの大きなデータを書き込み、エラーが発生しないこと、および書き込まれたバイト数が期待通りであることを確認します。このテストは、通常はスキップされ、-large_writeフラグが指定された場合にのみ実行されるようになっています。これは、コンソールへの大量出力がテスト環境に与える影響を考慮した設計です。

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

diff --git a/src/pkg/os/file_windows.go b/src/pkg/os/file_windows.go
index 9e0da5ae81..a86b8d61cd 100644
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -258,8 +258,18 @@ func (f *File) writeConsole(b []byte) (n int, err error) {
 	        f.lastbits = make([]byte, len(b))
 	        copy(f.lastbits, b)
 	}
-	if len(runes) > 0 {
-		uint16s := utf16.Encode(runes)
+	// syscall.WriteConsole seems to fail, if given large buffer.
+	// So limit the buffer to 16000 characters. This number was
+	// discovered by experimenting with syscall.WriteConsole.
+	const maxWrite = 16000
+	for len(runes) > 0 {
+		m := len(runes)
+		if m > maxWrite {
+			m = maxWrite
+		}
+		chunk := runes[:m]
+		runes = runes[m:]
+		uint16s := utf16.Encode(chunk)
 		for len(uint16s) > 0 {
 			var written uint32
 			err = syscall.WriteConsole(f.fd, &uint16s[0], uint32(len(uint16s)), &written, nil)
diff --git a/src/pkg/os/os_test.go b/src/pkg/os/os_test.go
index 14b4837a04..1940f562de 100644
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -6,6 +6,7 @@ package os_test
 
  import (
  	"bytes"
+	"flag"
  	"fmt"
  	"io"
  	"io/ioutil"
@@ -1066,3 +1067,31 @@ func TestDevNullFile(t *testing.T) {
  		t.Fatalf("wrong file size have %d want 0", fi.Size())
  	}
  }
+
+var testLargeWrite = flag.Bool("large_write", false, "run TestLargeWriteToConsole test that floods console with output")
+
+func TestLargeWriteToConsole(t *testing.T) {
+	if !*testLargeWrite {
+		t.Logf("skipping console-flooding test; enable with -large_write")
+		return
+	}
+	b := make([]byte, 32000)
+	for i := range b {
+		b[i] = '.'
+	}
+	b[len(b)-1] = '\n'
+	n, err := Stdout.Write(b)
+	if err != nil {
+		t.Fatalf("Write to os.Stdout failed: %v", err)
+	}
+	if n != len(b) {
+		t.Errorf("Write to os.Stdout should return %d; got %d", len(b), n)
+	}
+	n, err = Stderr.Write(b)
+	if err != nil {
+		t.Fatalf("Write to os.Stderr failed: %v", err)
+	}
+	if n != len(b) {
+		t.Errorf("Write to os.Stderr should return %d; got %d", len(b), n)
+	}
+}

コアとなるコードの解説

src/pkg/os/file_windows.go の変更

File.writeConsole関数は、Goのバイトスライスbを受け取り、それをWindowsコンソールに書き込む役割を担っています。

変更前は、runesスライス(Goの文字列をUnicodeコードポイントに変換したもの)が空でなければ、そのままutf16.EncodeでUTF-16にエンコードし、syscall.WriteConsoleに渡していました。

変更後は、以下のロジックが追加されました。

  1. const maxWrite = 16000: syscall.WriteConsoleが安定して処理できる最大文字数(UTF-16コードユニット数)を定義しています。この値は実験によって導き出されたものです。
  2. for len(runes) > 0 ループ: runesスライスが空になるまで、つまりすべての文字が処理されるまでループを続けます。
  3. チャンクサイズの決定:
    • m := len(runes): 現在残っているrunesスライスの長さをmに格納します。
    • if m > maxWrite { m = maxWrite }: もしmmaxWriteより大きければ、現在のチャンクサイズをmaxWriteに制限します。これにより、一度にmaxWrite文字を超えるデータがWriteConsoleに渡されるのを防ぎます。
  4. チャンクの抽出とスライスの更新:
    • chunk := runes[:m]: runesスライスの先頭からm文字分のチャンクを抽出します。
    • runes = runes[m:]: 処理されたm文字分をrunesスライスから削除し、次のイテレーションで残りの文字が処理されるようにします。
  5. チャンクのUTF-16エンコードと書き込み:
    • uint16s := utf16.Encode(chunk): 抽出したchunkをUTF-16エンコードします。
    • for len(uint16s) > 0 内部ループ: UTF-16エンコードされたデータがさらにuint16sスライスとして残っている限り、syscall.WriteConsoleを呼び出して書き込みを行います。syscall.WriteConsoleは、一度に書き込めるバイト数に制限があるため、この内部ループも必要です。

この修正により、Goのosパッケージは、Windowsコンソールへの大量のテキスト出力においても、WriteConsoleの潜在的な制限を回避し、堅牢な動作を保証できるようになりました。

src/pkg/os/os_test.go の変更

このファイルには、TestLargeWriteToConsoleという新しいテスト関数が追加されました。

  1. flag.Bool("large_write", false, ...): testLargeWriteというコマンドラインフラグを定義しています。このフラグがtrueに設定された場合にのみ、このテストが実行されます。これは、このテストが大量のコンソール出力を伴うため、通常のテストスイートの実行時間を不必要に長くしたり、テスト環境に影響を与えたりするのを避けるためです。
  2. テストのスキップ: if !*testLargeWriteの条件で、フラグが設定されていない場合はテストをスキップし、その旨をログに出力します。
  3. 大量データの生成: b := make([]byte, 32000)で32000バイトのバイトスライスを作成し、.で埋め尽くします。最後に改行文字\nを追加しています。これは、WriteConsoleが失敗する可能性のある「大きなバッファ」をシミュレートするためです。
  4. os.Stdoutへの書き込み: Stdout.Write(b)を呼び出し、生成した大量のデータを標準出力に書き込みます。エラーが発生しないこと、および書き込まれたバイト数が期待通り(32000バイト)であることを確認します。
  5. os.Stderrへの書き込み: 同様に、Stderr.Write(b)を呼び出し、標準エラー出力への書き込みもテストします。

このテストは、file_windows.goの変更が実際にWriteConsoleの制限を回避し、大量のデータ書き込みが成功することを確認するための重要な検証手段となります。

関連リンク

参考にした情報源リンク