[インデックス 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
メソッドは以下のように変更されました。
maxWrite
定数の導入:const maxWrite = 16000
という定数が導入されました。これは、一度にWriteConsole
に渡すUTF-16文字の最大数を示します。- チャンク処理:
len(runes) > 0
のループ内で、元のrune
スライスをmaxWrite
で定義されたサイズ以下の小さなチャンクに分割して処理するようになりました。m := len(runes)
で現在のrunes
スライスの長さを取得します。if m > maxWrite { m = maxWrite }
で、チャンクサイズがmaxWrite
を超えないように制限します。chunk := runes[:m]
で、現在のチャンクを抽出します。runes = runes[m:]
で、処理済みの部分を元のスライスから削除し、次のループで残りの部分を処理するようにします。
- UTF-16エンコードと書き込み: 各チャンクは
utf16.Encode(chunk)
によってUTF-16にエンコードされ、その後、syscall.WriteConsole
に渡されます。この内部ループは、UTF-16エンコードされたデータがさらにuint16s
スライスとして残っている限り続きます。
この変更により、Goプログラムがコンソールに大量のデータを書き込もうとした場合でも、内部的には小さなチャンクに分割されてWriteConsole
に渡されるため、APIの制限に引っかかることなく、安定して出力が完了するようになります。
また、この変更を検証するために、os_test.go
にTestLargeWriteToConsole
という新しいテストが追加されました。このテストは、Stdout
とStderr
に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
に渡していました。
変更後は、以下のロジックが追加されました。
const maxWrite = 16000
:syscall.WriteConsole
が安定して処理できる最大文字数(UTF-16コードユニット数)を定義しています。この値は実験によって導き出されたものです。for len(runes) > 0
ループ:runes
スライスが空になるまで、つまりすべての文字が処理されるまでループを続けます。- チャンクサイズの決定:
m := len(runes)
: 現在残っているrunes
スライスの長さをm
に格納します。if m > maxWrite { m = maxWrite }
: もしm
がmaxWrite
より大きければ、現在のチャンクサイズをmaxWrite
に制限します。これにより、一度にmaxWrite
文字を超えるデータがWriteConsole
に渡されるのを防ぎます。
- チャンクの抽出とスライスの更新:
chunk := runes[:m]
:runes
スライスの先頭からm
文字分のチャンクを抽出します。runes = runes[m:]
: 処理されたm
文字分をrunes
スライスから削除し、次のイテレーションで残りの文字が処理されるようにします。
- チャンクの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
という新しいテスト関数が追加されました。
flag.Bool("large_write", false, ...)
:testLargeWrite
というコマンドラインフラグを定義しています。このフラグがtrue
に設定された場合にのみ、このテストが実行されます。これは、このテストが大量のコンソール出力を伴うため、通常のテストスイートの実行時間を不必要に長くしたり、テスト環境に影響を与えたりするのを避けるためです。- テストのスキップ:
if !*testLargeWrite
の条件で、フラグが設定されていない場合はテストをスキップし、その旨をログに出力します。 - 大量データの生成:
b := make([]byte, 32000)
で32000バイトのバイトスライスを作成し、.
で埋め尽くします。最後に改行文字\n
を追加しています。これは、WriteConsole
が失敗する可能性のある「大きなバッファ」をシミュレートするためです。 os.Stdout
への書き込み:Stdout.Write(b)
を呼び出し、生成した大量のデータを標準出力に書き込みます。エラーが発生しないこと、および書き込まれたバイト数が期待通り(32000バイト)であることを確認します。os.Stderr
への書き込み: 同様に、Stderr.Write(b)
を呼び出し、標準エラー出力への書き込みもテストします。
このテストは、file_windows.go
の変更が実際にWriteConsole
の制限を回避し、大量のデータ書き込みが成功することを確認するための重要な検証手段となります。
関連リンク
- Go CL 6523043: https://golang.org/cl/6523043
参考にした情報源リンク
- Go Issue 3767:
os.Stdout.Write
fails on Windows when writing large amounts of data: https://github.com/golang/go/issues/3767 WriteConsole
function (consoleapi.h): https://learn.microsoft.com/en-us/windows/console/writeconsoleutf16
package - go.text/encoding/utf16: https://pkg.go.dev/golang.org/x/text/encoding/utf16syscall
package - golang.org/x/sys/windows: https://pkg.go.dev/golang.org/x/sys/windows