Open14

[勉強ログ] Go ならわかるシステムプログラミング

ピン留めされたアイテム
nukopynukopy

OS の役割

  • ファイル管理
  • メモリ管理
  • プロセス管理
  • I/O システム管理
  • ネットワーキング

書籍の全体像

  • 1 章
    • Go 言語の環境構築
    • システムプログラミングの概要
  • 2 ~ 4 章
    • Go 言語の抽象化の仕組み
    • Go 言語の抽象化により低レイヤが扱いやすくなっている例の紹介
      • io.Writer:出力の抽象化
      • io.Reader:入力の抽象化
      • チャネル:通知の抽象化
  • 5 章
    • システムコールとは何か
      • CPU のユーザモードから特権モードを必要とする操作をしたいときにシステムコールが必要(プロセスから API を介してカーネルへ特定の機能を要求する)
  • 6 ~ 8 章
    • ソケット通信の API
      • TCP/IP 通信の基礎をざっくり
      • TCP ソケットと HTTP の実装
      • UDP ソケットを使ったマルチキャスト通信
      • Unix ドメインソケット
  • 9 ~ 10 章
    • ファイルシステムの基礎
    • ファイルシステムの深堀り
      • ファイルの変更監視
      • ファイルロック
      • ファイルのメモリへのマッピング
      • 同期・非同期とブロッキング・ノンブロッキング
      • select 族のシステムコールによる I/O 多重化
      • FUSE を使った自作のファイルシステムの作成
  • 11 ~ 12 章
    • プロセス管理
      • プロセスの構成要素
      • Go から見たプロセス、OS から見たプロセス
      • シグナルによるプロセス間通信
        • シグナルの用途
          • プロセス間通信
          • ソフトウェア割り込み
        • 「シグナル」はシステムコールの逆
          • システムコール:ユーザ空間で動作しているプロセスからカーネル空間で動作しているカーネルに働きかけるためのインタフェース
          • シグナル:カーネル空間で動作しているカーネルからユーザ空間で動作しているプロセスに働きかけるためのインタフェース
  • 13 ~ 14 章
    • 並行、並列処理
      • スレッドと goroutine の違い
      • 並行、並列処理のデザパタ
  • 15 章
    • メモリ管理
      • 物理メモリと仮想メモリ
      • Go の配列、スライスのメモリの使い方
      • ガベージコレクション(GC)
      • 実行ファイルのメモリへの展開
  • 16 章
    • 時間と時刻(タイマーとクロック)
      • タイマー:決まった時間にコールバックをアプリケーションに返すもの
      • クロック(カウンター):現在時刻やそれ相当(プロセスやシステムの起動からの経過時間なども含む)を返すもの
  • 17 章
    • コンテナ技術
      • 仮想化は低レイヤの技術の組み合わせ
      • コンテナの仕組み
      • 自作コンテナ
      • コンテナは OS が持つリソースに「壁」を作り、そのプロセス専用の環境を作る。ネットワーク、ファイルシステム、プロセス、並列処理、メモリというこれまでの要素を全て扱うことでコンテナが実現されている。
  • 付録 A
    • セキュリティ関連の OS の機能
      • TLS のところはざっと読んどいた方が良い
nukopynukopy

勉強ログ

2022/01/09 02:30 ~ 04:50(2 h 20 m)

まえがき、あとがき、各章の最初の 1 節、各章のまとめを一通り読んで、本の全体像を把握した。

nukopynukopy

2022/04/22 21:15 ~ 25:00(3 h 30 m, 累計 2:20 + 3:30 = 5:50)

22:35 ~ 25:05 (2 h 30 m)

途中で「Go は静的なダックタイピング」という言葉に引っかかって色々調べてたら時間が溶けた。いっつも同じようなこと調べてるからもっと体系的に型システム周り勉強してから出直します。

  • ref: Goは何故継承する機能がないのでしょうか?

https://jp.quora.com/Goは何故継承する機能がないのでしょうか

Goではinterfaceが一致するかどうかで代入可能性が決まるという言語仕様を定めました。どんなメソッドが定義されているか、それが一致するかどうかによて代入可能かどうかが判定されます。事前に継承関係を宣言する必要はありません。これによって自由度が生まれます。

こういう型システムは、クラスの継承関係のような静的な関係ではなく、どんなメソッドを持つかといった型の構造によって代入可能性かどうかが決まるため、構造的部分型(structural subtyping)といいます。Goは構造的部分型なプログラミング言語のひとつです。

Go言語のような型システムは発表当時はけっこう珍しかったと思いますが、現代ではRustやTypescriptなども構造的部分型を採用しています。こういうことからも、これは良い判断だったと言えるのではないでしょうか。

  • ref: Why is there no type inheritance?

https://go.dev/doc/faq#inheritance

  • Go の復習
    • 値レシーバはコピーコストがある。なので基本構造体はポインタレシーバにしといた方が無難。
    • メソッド内でレシーバのフィールドを書き換えたい場合(副作用を持つレシーバを定義したい場合)、ポインタレシーバが必須。
    • Go のインタフェース=「具象型(構造体や組み込み型のエイリアス)が満たすべき仕様」。より具体的に言うと、具象型が持つべきメソッドの定義。

https://skatsuta.github.io/2015/12/29/value-receiver-pointer-receiver/

  • ファイルディスクリプタ
    • 意味
      • OS のカーネルが数値や文字列の読み書きを行う対象を抽象化するための識別子のこと。
      • OS によってプロセスが起動されたとき、3 つの疑似ファイルが作成され、それぞれにファイルディスクリプタを割り当てる。0 が標準入力、1 が標準出力、2 が標準エラー出力。
    • 何が嬉しい?
      • (仮。日本語おかしい。)OS のカーネルは、ファイル、標準入出力、ソケットなどの読み書きを行う対象を識別子として抽象化し、読み書きを行う際の対象を扱いやすくしている。

下記は fmt.Println("...") をデバッガで追って syscall.Write() を追ったときの様子。引数 fd はファイルディスクリプタを表し、値 1 は標準出力を表す。

ファイルディスクリプタを表す構造体は internal/poll/fd_unix.go/FD として以下のように定義されている。

type FD struct {
	// Lock sysfd and serialize access to Read and Write methods.
	fdmu fdMutex

	// System file descriptor. Immutable until Close.
	Sysfd int

	// I/O poller.
	pd pollDesc

	// Writev cache.
	iovecs *[]syscall.Iovec

	// Semaphore signaled when file is closed.
	csema uint32

	// Non-zero if this file has been set to blocking mode.
	isBlocking uint32

	// Whether this is a streaming descriptor, as opposed to a
	// packet-based descriptor like a UDP socket. Immutable.
	IsStream bool

	// Whether a zero byte read indicates EOF. This is false for a
	// message based socket connection.
	ZeroReadIsEOF bool

	// Whether this is a file rather than a network socket.
	isFile bool
}

21:15 ~ 22:15 (1 h)

  • 前回より大分空いてしまったので本の最初から 1 章を読み終わるまで
  • デバッガを使って fmt.Println("...") のコールグラフをシステムコールまでさかのぼった(UNIX 系 OS の場合、syscall/syscall_unix.go/Write
nukopynukopy

2022/04/23 11:37 ~ 20:20 (5:45, 累計 5:50 + 5:45 = 11:35)

  • Go の便利な抽象化
    • io.Writer 出力の抽象化
    • io.Reader 入力の抽象化
    • channel 通知の抽象化

17:45 ~ 20:20 (2 h 35 m)

ここまでで、io.Writer による出力の抽象化について勉強した。

https://github.com/nukopy/system-programming-in-go/commit/36fc3520c32f16a06eb639dded742abb0d788bcf

16:30 ~ 17:00 (30 m)

Go ならわかるシステムプログラミング Q2.3 標準出力にログを出力しつつ gzip 圧縮した JSON をレスポンスで返す

https://gist.github.com/nukopy/9bc0bf58d39670dd52a9034a3b3f0db4

14:45~ 16:20 (1 h 35 m)

...

11:37 ~ 12:42 (1 h 5 m)

  • 2 章続きから

io.Writer と fmt.Printf()

  • io/io.go で定義されているインタフェース io.Writer
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
	Write(p []byte) (n int, err error)
}

fmt.Printf() 内では、インタフェース io.Writer を満たす型 os.Stdout(標準出力のファイルディスクリプタ)を fmt.Fprintf() に渡している。fmt.Fprintf() 内では、ファイルディスクリプタの Write メソッドに書き込みたいバイト列を渡して呼んでいる。

  • fmt/print.go
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...any) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

os.Stdout の定義は以下の通り。Newfile 関数は *File を返す関数である。

  • os/file.go
// Stdin, Stdout, and Stderr are open Files pointing to the standard input,
// standard output, and standard error file descriptors.
//
// Note that the Go runtime writes to standard error for panics and crashes;
// closing Stderr may cause those messages to go elsewhere, perhaps
// to a file opened later.
var (
	Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
	Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
	Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

Newfile 関数の実装は以下の通り。OS ごとに実装が異なる。

  • os/file_unix.go
// NewFile returns a new File with the given file descriptor and
// name. The returned value will be nil if fd is not a valid file
// descriptor. On Unix systems, if the file descriptor is in
// non-blocking mode, NewFile will attempt to return a pollable File
// (one for which the SetDeadline methods work).
//
// After passing it to NewFile, fd may become invalid under the same
// conditions described in the comments of the Fd method, and the same
// constraints apply.
func NewFile(fd uintptr, name string) *File {
	kind := kindNewFile
	if nb, err := unix.IsNonblock(int(fd)); err == nil && nb {
		kind = kindNonBlock
	}
	return newFile(fd, name, kind)
}

File 型はファイルディスクリプタを抽象化した型である。*file で OS ごとの違いを吸収する。

  • os/types.go
// File represents an open file descriptor.
type File struct {
	*file // os specific
}

file は OS により実装が異なる。UNIX 系 OS では以下のようになる。

  • os/file_unix.go
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
	pfd         poll.FD
	name        string
	dirinfo     *dirInfo // nil unless directory being read
	nonblock    bool     // whether we set nonblocking mode
	stdoutOrErr bool     // whether this is stdout or stderr
	appendMode  bool     // whether file is opened for appending
}

Windows だと以下のようになる。

  • os/file_windows.go
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
	pfd        poll.FD
	name       string
	dirinfo    *dirInfo // nil unless directory being read
	appendMode bool     // whether file is opened for appending
}

ファイルディスクリプタの Write メソッド

  • os/file.go
// Write writes len(b) bytes from b to the File.
// It returns the number of bytes written and an error, if any.
// Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
	if err := f.checkValid("write"); err != nil {
		return 0, err
	}
	n, e := f.write(b)
	if n < 0 {
		n = 0
	}
	if n != len(b) {
		err = io.ErrShortWrite
	}

	epipecheck(f, e)

	if e != nil {
		err = f.wrapErr("write", e)
	}

	return n, err
}

ファイルディスクリプタの write メソッド

  • os/file_posix.go
// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int, err error) {
	n, err = f.pfd.Write(b)
	runtime.KeepAlive(f)
	return n, err
}

f.pfdpoll.FD である。その実体は以下の通り。OS ごとに異なる。

  • internal/poll/fd_unix.go
// FD is a file descriptor. The net and os packages use this type as a
// field of a larger type representing a network connection or OS file.
type FD struct {
	// Lock sysfd and serialize access to Read and Write methods.
	fdmu fdMutex

	// System file descriptor. Immutable until Close.
	Sysfd int

	// I/O poller.
	pd pollDesc

	// Writev cache.
	iovecs *[]syscall.Iovec

	// Semaphore signaled when file is closed.
	csema uint32

	// Non-zero if this file has been set to blocking mode.
	isBlocking uint32

	// Whether this is a streaming descriptor, as opposed to a
	// packet-based descriptor like a UDP socket. Immutable.
	IsStream bool

	// Whether a zero byte read indicates EOF. This is false for a
	// message based socket connection.
	ZeroReadIsEOF bool

	// Whether this is a file rather than a network socket.
	isFile bool
}
nukopynukopy

2022/04/24 14:00 ~ 3:02 (4:20, 累計 11:35 + 4:20 = 15:55)

  • io.Writer 出力の抽象化について図示してみた

23:47 ~ 3:02 (3:15)

3 章に入る。

syscall.Read の実装

  • syscall/syscall_unix.go
func Read(fd int, p []byte) (n int, err error) {
	n, err = read(fd, p)
	if race.Enabled {
		if n > 0 {
			race.WriteRange(unsafe.Pointer(&p[0]), n)
		}
		if err == nil {
			race.Acquire(unsafe.Pointer(&ioSync))
		}
	}
	if msanenabled && n > 0 {
		msanWrite(unsafe.Pointer(&p[0]), n)
	}
	if asanenabled && n > 0 {
		asanWrite(unsafe.Pointer(&p[0]), n)
	}
	return
}

read の実装

  • syscall/zsyscall_linux_amd64.go
func read(fd int, p []byte) (n int, err error) {
	var _p0 unsafe.Pointer
	if len(p) > 0 {
		_p0 = unsafe.Pointer(&p[0])
	} else {
		_p0 = unsafe.Pointer(&_zero)
	}
	r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
	n = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}

Syscall の実装

  • syscall/asm_linux_amd64.s
//
// System calls for AMD64, Linux
//

#define SYS_gettimeofday 96

// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT ·Syscall(SB),NOSPLIT,$0-56
	CALL	runtime·entersyscall(SB)
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	CALL	runtime·exitsyscall(SB)
	RET
ok:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	CALL	runtime·exitsyscall(SB)
	RET

ref: 解説 & 翻訳 - A Quick Guide to Go's Assembler

Go のビルド周りの仕組みがめちゃめちゃ分かりやすい。

https://zenn.dev/hsaki/articles/godoc-asm-ja

14:00 ~ 15:05 (1:05)

io.Writer 出力の抽象化について図示してたみ

  • io.Writer 出力の抽象化

プログラムからシステムコール write を呼ぶ

Go のプログラムからシステムコール write を呼ぶ(io.Writer による抽象化)
(プログラマが理解するのは io.Writer の仕様までで良い)

nukopynukopy

用語

  • ファイルディスクリプタ
  • インタフェース
  • バッファ(bytes.Buffer の実体は何?)
nukopynukopy
  • 前回 2022/04/24 14:00 ~ 3:02 (4:20, 累計 11:35 + 4:20 = 15:55)

2022/06/12 10:35 ~ : (4:20, 累計 15:55 + )

10:35 ~ 12:35 2h

  • 1, 2 章の復習

12:45 ~ 13:30 45m

  • 3 章の前半呼んだ

15:00 ~ 17:00 2h

  • bytes.Buffer の Read / Write あたりのソースコードを見て整理した
    • bytes.Buffer.Read
      • 自身(bytes.Buffer インスタンス)のバッファから他のバッファへの読み込み(コピー)
      • 実体:copy(p, b.buf[b\.off:]) (copy(dst, src))
    • bytes.Buffer.Write
      • 他のバッファから自身(bytes.Buffer.buf インスタンス)への書き込み(コピー)
      • 実体:copy(b.buf[m:], p)
  • 実体はどちらもバッファ間のデータのコピー

18:00 ~ 19:00 1h

  • 3 章を一気に読んだ。でも理解できてない。
  • 4 章の導入だけ読んで goroutine で並列処理を行うサンプルコードを動かした
nukopynukopy

io.Reader

type Reader interface {
        func Read(p []byte) (n int, err error)
}

io.Reader の使用の流れ

「バッファを作り、それに対してデータを読み込む」という流れ。

// 1024 バイトのバッファを make で作る
buffer := make([]byte, 1024)
// size は実際に読み込んだバイト数、err はエラー
size, err := r.Read(buffer)
  1. 読み込むデータの格納場所を事前に用意:p := make([]s, ....)
  2. メソッド io.Reader.Read(p)
  3. (読み込んだ内容を書き込み)
  4. 読み込んだバイト数、エラーを返す

io.Reader インタフェースを満たす構造体の例:bytes.Buffer

  • bytes.Buffer
    • Read / Writer メソッドを持つ可変バイト列
    • メンバー
      • Buffer.buf:バイト列
      • off
        • Read -> 読み込み開始位置
        • Write
// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
	buf      []byte // contents are the bytes buf[off : len(buf)]
	off      int    // read at &buf[off], write at &buf[len(buf)]
	lastRead readOp // last read operation, so that Unread* can work correctly.
}
  • bytes.Buffer.Read
    • (自身が保持している)バッファのデータを他のバッファへ読み出す。下記実装を見て分かる通り、Buffer.Read が実行されると読み出し開始位置は読み出したデータは消費される。
    • (このメソッドの実装により bytes.Buffer は `io.Reader インタフェースを満たす)
// Read reads the next len(p) bytes from the buffer or until the buffer
// is drained. The return value n is the number of bytes read. If the
// buffer has no data to return, err is io.EOF (unless len(p) is zero);
// otherwise it is nil.
func (b *Buffer) Read(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	if b.empty() {
		// Buffer is empty, reset to recover space.
		b.Reset()
		if len(p) == 0 {
			return 0, nil
		}
		return 0, io.EOF
	}

	// b.buf を p へコピー
	n = copy(p, b.buf[b.off:])
	// func copy(dst []Type, src []Type) int
	// copy はコピーされた要素の長さを返す min(len(dst), len(src))

	// 読み出し開始位置をコピーした分だけすすめる
	b.off += n
	if n > 0 {
		b.lastRead = opRead
	}
	return n, nil
}
  • bytes.Buffer.Write
    • (自身が保持している)バッファへバイト列を書き込む
    • (このメソッドの実装により bytes.Buffer は `io.Wirter インタフェースを満たす)
// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	m, ok := b.tryGrowByReslice(len(p))
	if !ok {
		m = b.grow(len(p))
	}
	return copy(b.buf[m:], p), nil
}
  • bytes.Buffer.String

// String returns the contents of the unread portion of the buffer
// as a string. If the Buffer is a nil pointer, it returns "<nil>".
//
// To build strings more efficiently, see the strings.Builder type.
func (b *Buffer) String() string {
	if b == nil {
		// Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

例:bytes.Buffer の Write / Read のサンプル

import (
	"bytes"
	"fmt"
)

func main() {
	var buf1 bytes.Buffer
	buf2 := make([]byte, 4)

	// by1 を buf1 へ書き込む
	by1 := []byte("0123456789")
	fmt.Printf("by1  : %s\n", string(by1))
	fmt.Printf("buf1 : %s\n", buf1.String())
	fmt.Printf("Write by1 to buf1...\n")
	n, err := buf1.Write(by1)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%d bytes were written to buf1.\n", n)
	fmt.Printf("by1  : %s\n", string(by1)) // Write は by1 には影響しない
	fmt.Printf("buf1 : %s\n", buf1.String())
	fmt.Println()

	// buf1 から buf2 へ読み込む
	fmt.Printf("buf1 : %s\n", buf1.String())
	fmt.Printf("buf2 : %s\n", string(buf2))
	fmt.Printf("Read from buf1 to buf2...\n")
	n2, err2 := buf1.Read(buf2) // buf1 から buf2 へ文字列を読み込む
	if err2 != nil {
		panic(err2)
	}
	fmt.Printf("%d bytes were read from buf1 to buf2.\n", n2)
	fmt.Printf("buf1 : %s\n", buf1.String())
	fmt.Printf("buf2 : %s\n", string(buf2))
	fmt.Println()

	// buf1 から by2 へ読み込む
	by2 := []byte("XXXXXXXXXX")
	fmt.Printf("buf1 : %s\n", buf1.String())
	fmt.Printf("by2   : %s\n", string(by2))
	fmt.Printf("Read buf1 to by2...\n")
	n3, err3 := buf1.Read(by2)
	if err3 != nil {
		panic(err3)
	}
	fmt.Printf("%d bytes were read from buf1 to by2.\n", n3)
	fmt.Printf("buf1 : %s\n", buf1.String()) // buf1 が保持しているデータが全て by2 に移されているため、buf1 は空になる
	fmt.Printf("by2   : %s\n", string(by2))
}

出力

by1  : 0123456789
buf1 : 
Write by1 to buf1...
10 bytes were written to buf1.
by1  : 0123456789
buf1 : 0123456789

buf1 : 0123456789
buf2 : 
Read from buf1 to buf2...
4 bytes were read from buf1 to buf2.
buf1 : 456789
buf2 : 0123

buf1 : 456789
by2   : XXXXXXXXXX
Read buf1 to by2...
6 bytes were read from buf1 to by2.
buf1 : 
by2   : 456789XXXX
nukopynukopy

Q&A

これに答えられたら勉強できた証

  • POSIX 系 OS とは?
  • defer file.Close() を行わないと何が良くない?
nukopynukopy

Q&A

  • sec 3.5
    • ネットワーク上で転送されるデータがビッグエンディアンなのはなぜ?歴史的経緯?
nukopynukopy

参考資料

PNG 解析

一番参考になった
https://news.mynavi.jp/techplus/article/gogogo-13/

https://news.mynavi.jp/techplus/article/gogogo-13/

https://qiita.com/jh_0606/items/56a43bd9f67d05c0e7f4

CRC とは

データの破損チェック用の値

CRCとは、誤り検出方式の一つで、データを値とみなしてある定数で割った余り(余剰)を用いて誤りの検知を行なうもの。その検査用の値をCRC値、CRC符号、巡回冗長符号などと呼ぶが、値自体をCRC(Cyclic Redundancy Code)と呼ぶこともある。

nukopynukopy

os.file.Seek(offset, whence)

Seek は、次のファイルへの Read または Write のオフセットを offset に設定し、whence に従って解釈する。

  • whence の値の意味
    • 0:ファイルの原点からの相対的な移動
    • 1:現在のオフセットからの移動
    • 2:終点からの移動

新しいオフセットと、もしあればエラーが返される。
O_APPEND でオープンされたファイルに対する Seek の動作は指定されていない。

file がディレクトリの場合、Seekの動作は OS によって異なる。Unix 系 OS では、ディレクトリの先頭までシークできるが、Windows ではできない。

補足

whence: 「どこから」を意味する疑問副詞

nukopynukopy

あーーー binary.Read の意味をずっと勘違いしてたー

binary.Read

  • binary.Read
    • 引数として、io.Reader、reader に格納されたデータのバイトオーダー(エンディアン)、reader に格納されたデータのエンディアンによる変換結果を格納する変数のポインタを渡す。実行すると、変数の byte 数分だけ reader からデータが読まれ、指定したバイトオーダに従って変換されたあとのデータが第 3 引数に渡した変数に格納される。なお、第 3 引数にわたすのは変数のポインタであることに注意。
var n int32
binary.Read(reader, binary.BigEndian, &n)

Go によるバイナリ解析の例:PNG 画像の読み込み

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	// 画像ファイルを io.Reader として読み込む
	filename := "Lenna.png"
	file, err := os.Open(filename)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	// PNG ファイルの解析
	fmt.Printf("Analyzing .png file \"%s\"...\n", filename)
	chunks, err := analyzePngFile(file)
	if err != nil {
		panic(err)
	}

	// chunk の概要を出力
	for _, chunk := range chunks {
		dumpPngChunk(chunk)
	}
}

func analyzePngFile(file *os.File) ([]io.Reader, error) {
	// PNG シグネチャの読み込み
	lengthPngSign := 8
	buffer := make([]byte, lengthPngSign)
	pngSign := "\x89PNG\r\n\x1a\n"

	file.Read(buffer)
	if !bytes.Equal([]byte(pngSign), buffer) {
		return nil, fmt.Errorf("this file format is not .png")
	}

	// chunk の分割
	// PNG のチャンク = データ長(4 byte) + 種類(4 byte) + データ(xxx byte) + CRC(4 byte)
	var chunks []io.Reader // PNG の各チャンクを格納する配列
	var currentOffset int64 = 8

	for {
		var length int32 // 4 byte
		err := binary.Read(file, binary.BigEndian, &length) // file(io.Reader) から 4 byte 分読み込んで int32 の変数に格納する
		if err == io.EOF {
			break
		}

		chunks = append(chunks, io.NewSectionReader(file, currentOffset, int64(length)+12)) // offset から何 byte 読むか

		// 次のチャンクの先頭に移動
		// 現在位置は長さを読み終わった箇所なので、
		// チャンク名(4 バイト) + データ長 + CRC(4 バイト)先に移動
		currentOffset, err = file.Seek(int64(length+8), 1)
		if err != nil {
			panic(err)
		}
	}
	fmt.Printf("%d chunks gotten!\n", len(chunks))

	return chunks, nil
}

func dumpPngChunk(chunk io.Reader) {
	// チャンクの先頭を読み込む
	var length int32
	binary.Read(chunk, binary.BigEndian, &length)

	// チャンクの種類を読み込む
	buffer := make([]byte, 4)
	_, err := chunk.Read(buffer)
	if err != nil {
		panic(err)
	}

	fmt.Printf("chunk '%v' (%d bytes)\n", string(buffer), length)
}