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

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

このコミットは、Go言語のsyscallパッケージ、特にWindows固有のシステムコールにおいて、関数のエラー返却メカニズムを改善するものです。以前はエラーを示すためにuintptr型が使用されていましたが、この変更によりGoの標準的なerrorインターフェースを返すように統一されます。これにより、エラーハンドリングが一貫性のある、よりGoらしい方法で行えるようになります。

コミット

commit ef65feda2afd3644c884c630d628e46b06082e4c
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Thu Dec 8 12:07:21 2011 +1100

    syscall: return error, not uintptr, when function returns error
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5450119

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

https://github.com/golang/go/commit/ef65feda2afd3644c884c630d628e46b06082e4c

元コミット内容

このコミットの目的は、Goのsyscallパッケージにおいて、Windowsシステムコールがエラーを返す際に、従来のuintptr型ではなく、Goの標準的なerrorインターフェースを返すように変更することです。これにより、Goのエラーハンドリングの慣習に沿った、より自然で安全なエラー処理が可能になります。

変更の背景

Go言語は、エラーハンドリングにおいて多値戻り値とerrorインターフェースの使用を推奨しています。しかし、初期のGoのsyscallパッケージ、特にWindows固有の実装では、システムコールが成功した場合は0、失敗した場合はエラーコードをuintptr型で返すという、C言語やWindows APIの慣習に近い形式が採用されていました。

このアプローチにはいくつかの問題がありました。

  1. Goのエラーハンドリングとの不整合: uintptrでエラーを表現することは、Goの慣習であるif err != nilによるエラーチェックと異なり、コードの可読性や一貫性を損ねていました。
  2. 型安全性と表現力不足: uintptrは単なるポインタまたは整数であり、それがエラーであることを明示的に示すものではありませんでした。エラーの種類や詳細な情報をカプセル化するGoのerrorインターフェースの利点を活かせませんでした。
  3. 移植性の問題: Windows固有のuintptrによるエラー表現は、他のOS(Linux, macOSなど)のsyscallパッケージにおけるエラーハンドリングと異なり、コードの移植性を低下させる可能性がありました。

このコミットは、これらの問題を解決し、Goのエラーハンドリングのベストプラクティスに準拠するために行われました。

前提知識の解説

Go言語のエラーハンドリング

Go言語では、エラーは通常、関数の最後の戻り値としてerrorインターフェース型で返されます。エラーがない場合はnilが返され、呼び出し元はif err != nilという慣用句でエラーの有無をチェックします。

func doSomething() (resultType, error) {
    // ... 処理 ...
    if somethingWentWrong {
        return zeroValue, errors.New("something went wrong")
    }
    return actualResult, nil
}

// 呼び出し側
res, err := doSomething()
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println("Result:", res)

uintptr

uintptrは、ポインタを保持するのに十分な大きさの符号なし整数型です。Goでは、C言語のポインタやWindows APIのハンドルなど、低レベルのメモリ操作やシステムコールとの連携で一時的に使用されることがあります。しかし、これは型安全性が低く、通常のエラー表現には推奨されません。

syscall.Errno

Goのsyscallパッケージには、OS固有のエラーコードを表現するためのErrno型が存在します。これは、システムコールが返す数値エラーコードをGoの型システムでラップしたものです。このコミット以前は、Errnouintptrのエイリアスとして定義され、そのError()メソッドでエラーメッセージを生成していました。このコミットでは、Errnouintptrから独立した型として定義され、errorインターフェースを実装するようになります。

Windows APIのエラーハンドリング

Windows API関数は、通常、成功/失敗を示す戻り値を持ち、詳細なエラー情報はGetLastError()関数を呼び出すことで取得できるエラーコード(DWORD型)として提供されます。このエラーコードは、システム定義のエラーメッセージに対応しています。

技術的詳細

このコミットの主要な変更点は、GoのsyscallパッケージにおけるWindowsシステムコールの戻り値の型を、エラーを示すuintptrintから、Goの標準的なerrorインターフェースに変更したことです。

具体的には、以下の点が変更されました。

  1. //sysディレクティブの変更: src/pkg/exp/wingui/winapi.gosrc/pkg/syscall/syscall_windows.goなどのファイルで、システムコール関数の宣言に付与されている//sysディレクティブが修正されました。 例:

    • //sys GetModuleHandle(modname *uint16) (handle syscall.Handle, errno int) = GetModuleHandleW//sys GetModuleHandle(modname *uint16) (handle syscall.Handle, err error) = GetModuleHandleW に変更されました。
    • //sys RegOpenKeyEx(...) (regerrno uintptr) = advapi32.RegOpenKeyExW//sys RegOpenKeyEx(...) (regerrno error) = advapi32.RegOpenKeyExW に変更されました。 これにより、mksyscall_windows.plスクリプトが生成するGoコードにおいて、エラーがerror型として扱われるようになります。
  2. syscall.Errno型の再定義と実装: src/pkg/syscall/dll_windows.goにあったErrno型の定義がsrc/pkg/syscall/syscall_windows.goに移動され、その定義がtype Errno uintptrから、errorインターフェースを実装する具体的な型として再定義されました。 Errno型は、Error()メソッドを持つことでerrorインターフェースを満たします。このError()メソッドは、Windows APIのFormatMessage関数を呼び出して、数値エラーコードに対応する人間が読めるエラーメッセージを取得します。

  3. システムコールラッパーの変更: src/pkg/exp/wingui/zwinapi.gosrc/pkg/syscall/zsyscall_windows_386.gosrc/pkg/syscall/zsyscall_windows_amd64.goなどの自動生成されるシステムコールラッパー関数が変更されました。

    • 以前は、システムコールが失敗した場合にuintptr型のエラーコードを直接返したり、errno intとして返したりしていました。
    • 変更後は、システムコールが失敗した場合に、返されたエラーコード(uintptr)をsyscall.Errno型にキャストし、それをerrorインターフェースとして返すようになりました。成功した場合はnilを返します。 例:
    // 変更前 (zwinapi.goのGetModuleHandleの一部)
    // if handle == 0 {
    //     if e1 != 0 {
    //         errno = int(e1)
    //     } else {
    //         errno = syscall.EINVAL
    //     }
    // } else {
    //     errno = 0
    // }
    
    // 変更後
    // if handle == 0 {
    //     if e1 != 0 {
    //         err = error(e1) // e1はsyscall.Errno型
    //     } else {
    //         err = syscall.EINVAL
    //     }
    // }
    

    ここでe1syscall.Syscallが返す第三の戻り値で、Windowsのエラーコード(uintptr)です。これがsyscall.Errnoに変換され、最終的にerrorとして返されます。

  4. 呼び出し箇所の修正: src/pkg/exp/wingui/gui.gosrc/pkg/mime/type_windows.gosrc/pkg/net/fd_windows.gosrc/pkg/net/interface_windows.gosrc/pkg/net/lookup_windows.goなど、syscallパッケージの関数を呼び出している箇所が修正されました。

    • 以前はif e != 0のように数値でエラーをチェックしていましたが、
    • 変更後はif e != nilのようにGoの標準的なエラーチェックを行うようになりました。
    • また、os.NewSyscallErrorの引数も、syscall.Errno(e)のようにErrno型にキャストして渡すように変更されています。
  5. mksyscall_windows.plスクリプトの変更: このPerlスクリプトは、//sysディレクティブに基づいてGoのシステムコールラッパーコードを生成します。このコミットでは、スクリプトがerror型の戻り値を正しく処理し、適切なエラーチェックとErrnoへの変換を行うコードを生成するように修正されました。

これらの変更により、GoのWindowsシステムコールは、Goのエラーハンドリングの原則に完全に準拠するようになり、より堅牢で読みやすいコードが書けるようになりました。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/syscall/syscall_windows.go:

    • Errno型の定義がsrc/pkg/syscall/dll_windows.goから移動され、Error()メソッドの実装が追加されました。これにより、Errnoerrorインターフェースを直接実装するようになりました。
    • //sysディレクティブを持つ多くのシステムコール関数の宣言で、エラー戻り値の型がuintptrintからerrorに変更されました。
    • errstr関数が削除され、そのロジックがErrno.Error()メソッドに統合されました。
  2. src/pkg/syscall/dll_windows.go:

    • Errno型の定義が削除されました。
  3. src/pkg/syscall/mksyscall_windows.pl:

    • システムコールラッパーを生成するPerlスクリプトが修正され、//sysディレクティブでerrorが指定された場合に、適切なエラーチェックとsyscall.Errnoへの変換を行うGoコードを生成するようになりました。
  4. src/pkg/syscall/zsyscall_windows_386.go および src/pkg/syscall/zsyscall_windows_amd64.go:

    • mksyscall_windows.plによって自動生成されるファイルであり、多くのシステムコールラッパー関数において、エラーの戻り値がuintptrからerrorに変更され、内部でsyscall.Errnoへの変換が行われるようになりました。
    • 例: GetLastError()関数の戻り値がuintptrからerrorに変更され、r0 != 0の場合にErrno(r0)を返すようになりました。
  5. src/pkg/exp/wingui/gui.go および src/pkg/exp/wingui/winapi.go:

    • winguiパッケージはWindows GUIの実験的な実装であり、多くのWindows APIを呼び出します。これらのファイルでは、システムコール関数の呼び出し箇所でエラーチェックがe != 0からe != nilに変更され、エラー変数の型もintからerrorに変更されました。
    • winapi.goでは、//sysディレクティブの変更が多数行われました。
  6. src/pkg/mime/type_windows.gosrc/pkg/net/fd_windows.gosrc/pkg/net/interface_windows.gosrc/pkg/net/lookup_windows.go:

    • これらのファイルでは、syscallパッケージの関数呼び出しにおけるエラーチェックが!= 0から!= nilに変更されました。

コアとなるコードの解説

syscall/syscall_windows.goErrno 型の変更

// 変更前 (dll_windows.go にあった定義)
// type Errno uintptr
// func (e Errno) Error() string { ... }

// 変更後 (syscall_windows.go に移動)
type Errno uintptr

func (e Errno) Error() string {
	// deal with special go errors
	idx := int(e - APPLICATION_ERROR)
	if 0 <= idx && idx < len(errors) {
		return errors[idx]
	}
	// ask windows for the remaining errors
	var flags uint32 = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_IGNORE_INSERTS
	b := make([]uint16, 300)
	n, err := FormatMessage(flags, 0, uint32(e), 0, b, nil)
	if err != nil {
		return "error " + itoa(int(e)) + " (FormatMessage failed with err=" + itoa(int(err.(Errno))) + ")"
	}
	// trim terminating \r and \n
	for ; n > 0 && (b[n-1] == '\n' || b[n-1] == '\r'); n-- {
	}
	return string(utf16.Decode(b[:n]))
}

この変更により、syscall.Errnouintptrのエイリアスでありながら、Error()メソッドを実装することでGoのerrorインターフェースを満たすようになりました。Error()メソッドは、Windows APIのFormatMessage関数を使用して、数値エラーコードを人間が読める文字列に変換します。これにより、システムコールから返されるエラーが、Goの標準的なエラー処理メカニズムにシームレスに統合されます。

mksyscall_windows.pl の変更

# 変更前 (一部抜粋)
# if ($rettype eq "errno") {
#     $body .= "\t\t\t$name = int(e1)\\n";
# }

# 変更後 (一部抜粋)
# ...
# } elsif($rettype eq "error") {
#    # Set $reg to "error" only if returned value indicate failure
#    $body .= "\\tif $reg != 0 {\\n";
#    $body .= "\\t\\t$name = Errno($reg)\\n";
#    $body .= "\\t}\\n";
# }

このPerlスクリプトの変更は、//sysディレクティブで戻り値の型がerrorと指定された場合に、生成されるGoコードが、システムコールからの生の戻り値(uintptr)をsyscall.Errno型にキャストし、それをerrorとして返すように指示します。これにより、自動生成されるラッパー関数がGoのエラーハンドリング規約に準拠するようになります。

自動生成される zsyscall_windows_*.go ファイルの変更例

zsyscall_windows_386.gozsyscall_windows_amd64.go のようなファイルでは、各システムコールラッパー関数が以下のように変更されました。

// 変更前 (例: GetLastError)
// func GetLastError() (lasterr uintptr) {
//     r0, _, _ := Syscall(procGetLastError.Addr(), 0, 0, 0, 0)
//     lasterr = uintptr(r0)
//     return
// }

// 変更後 (例: GetLastError)
func GetLastError() (lasterr error) {
	r0, _, _ := Syscall(procGetLastError.Addr(), 0, 0, 0, 0)
	if r0 != 0 {
		lasterr = Errno(r0)
	}
	return
}

この変更により、GetLastErrorのような関数は、エラーがない場合はnilを返し、エラーがある場合はsyscall.Errno型のインスタンス(これはerrorインターフェースを満たす)を返すようになりました。これにより、呼び出し元はif err != nilというGoの標準的な方法でエラーをチェックできます。

wingui パッケージでのエラーチェックの変更例

src/pkg/exp/wingui/gui.go のようなファイルでは、システムコール呼び出し後のエラーチェックが変更されました。

// 変更前
// var e int
// ...
// if e != 0 {
//     abortErrNo("CreateWindowEx", e)
// }

// 変更後
// var e error
// ...
// if e != nil {
//     abortErrNo("CreateWindowEx", e)
// }

この変更は、syscallパッケージの関数がerrorインターフェースを返すようになったことに伴い、呼び出し側もその変更に適応したことを示しています。これにより、Goのコード全体で一貫したエラーハンドリングが実現されます。

関連リンク

  • Go言語のエラーハンドリングに関する公式ドキュメントやブログ記事(当時のものがあれば)
  • Windows APIのエラーコードに関するMicrosoftのドキュメント
  • Goのsyscallパッケージのドキュメント

参考にした情報源リンク