[勉強ログ] Go ならわかるシステムプログラミング
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 ドメインソケット
- ソケット通信の API
- 9 ~ 10 章
- ファイルシステムの基礎
- ファイルシステムの深堀り
- ファイルの変更監視
- ファイルロック
- ファイルのメモリへのマッピング
- 同期・非同期とブロッキング・ノンブロッキング
- select 族のシステムコールによる I/O 多重化
- FUSE を使った自作のファイルシステムの作成
- 11 ~ 12 章
- プロセス管理
- プロセスの構成要素
- Go から見たプロセス、OS から見たプロセス
- シグナルによるプロセス間通信
- シグナルの用途
- プロセス間通信
- ソフトウェア割り込み
- 「シグナル」はシステムコールの逆
- システムコール:ユーザ空間で動作しているプロセスからカーネル空間で動作しているカーネルに働きかけるためのインタフェース
- シグナル:カーネル空間で動作しているカーネルからユーザ空間で動作しているプロセスに働きかけるためのインタフェース
- シグナルの用途
- プロセス管理
- 13 ~ 14 章
- 並行、並列処理
- スレッドと goroutine の違い
- 並行、並列処理のデザパタ
- 並行、並列処理
- 15 章
- メモリ管理
- 物理メモリと仮想メモリ
- Go の配列、スライスのメモリの使い方
- ガベージコレクション(GC)
- 実行ファイルのメモリへの展開
- メモリ管理
- 16 章
- 時間と時刻(タイマーとクロック)
- タイマー:決まった時間にコールバックをアプリケーションに返すもの
- クロック(カウンター):現在時刻やそれ相当(プロセスやシステムの起動からの経過時間なども含む)を返すもの
- 時間と時刻(タイマーとクロック)
- 17 章
- コンテナ技術
- 仮想化は低レイヤの技術の組み合わせ
- コンテナの仕組み
- 自作コンテナ
- コンテナは OS が持つリソースに「壁」を作り、そのプロセス専用の環境を作る。ネットワーク、ファイルシステム、プロセス、並列処理、メモリというこれまでの要素を全て扱うことでコンテナが実現されている。
- コンテナ技術
- 付録 A
- セキュリティ関連の OS の機能
- TLS のところはざっと読んどいた方が良い
- セキュリティ関連の OS の機能
勉強ログ
2022/01/09 02:30 ~ 04:50(2 h 20 m)
まえがき、あとがき、各章の最初の 1 節、各章のまとめを一通り読んで、本の全体像を把握した。
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は何故継承する機能がないのでしょうか?
Goではinterfaceが一致するかどうかで代入可能性が決まるという言語仕様を定めました。どんなメソッドが定義されているか、それが一致するかどうかによて代入可能かどうかが判定されます。事前に継承関係を宣言する必要はありません。これによって自由度が生まれます。
こういう型システムは、クラスの継承関係のような静的な関係ではなく、どんなメソッドを持つかといった型の構造によって代入可能性かどうかが決まるため、構造的部分型(structural subtyping)といいます。Goは構造的部分型なプログラミング言語のひとつです。
Go言語のような型システムは発表当時はけっこう珍しかったと思いますが、現代ではRustやTypescriptなども構造的部分型を採用しています。こういうことからも、これは良い判断だったと言えるのではないでしょうか。
- ref: Why is there no type inheritance?
- Go の復習
- 値レシーバはコピーコストがある。なので基本構造体はポインタレシーバにしといた方が無難。
- メソッド内でレシーバのフィールドを書き換えたい場合(副作用を持つレシーバを定義したい場合)、ポインタレシーバが必須。
- Go のインタフェース=「具象型(構造体や組み込み型のエイリアス)が満たすべき仕様」。より具体的に言うと、具象型が持つべきメソッドの定義。
- ファイルディスクリプタ
- 意味
- 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
)
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
による出力の抽象化について勉強した。
16:30 ~ 17:00 (30 m)
Go ならわかるシステムプログラミング Q2.3 標準出力にログを出力しつつ gzip 圧縮した JSON をレスポンスで返す
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.pfd
は poll.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
}
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 のビルド周りの仕組みがめちゃめちゃ分かりやすい。
14:00 ~ 15:05 (1:05)
io.Writer
出力の抽象化について図示してたみ
-
io.Writer
出力の抽象化
プログラムからシステムコール write を呼ぶ
Go のプログラムからシステムコール write を呼ぶ(io.Writer
による抽象化)
(プログラマが理解するのは io.Writer
の仕様までで良い)
用語
- ファイルディスクリプタ
- インタフェース
- バッファ(
bytes.Buffer
の実体は何?)
- 前回 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 で並列処理を行うサンプルコードを動かした
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)
- 読み込むデータの格納場所を事前に用意:
p := make([]s, ....)
- メソッド
io.Reader.Read(p)
- (読み込んだ内容を書き込み)
- 読み込んだバイト数、エラーを返す
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
Q&A
これに答えられたら勉強できた証
- POSIX 系 OS とは?
-
defer file.Close()
を行わないと何が良くない?
個人的に引っかかったところ
- バッファとは何か?その役割は?実例は?
- buffer の場合、入出力の主語がややこしくなる
Q&A
- sec 3.5
- ネットワーク上で転送されるデータがビッグエンディアンなのはなぜ?歴史的経緯?
参考資料
PNG 解析
一番参考になった
CRC とは
データの破損チェック用の値
CRCとは、誤り検出方式の一つで、データを値とみなしてある定数で割った余り(余剰)を用いて誤りの検知を行なうもの。その検査用の値をCRC値、CRC符号、巡回冗長符号などと呼ぶが、値自体をCRC(Cyclic Redundancy Code)と呼ぶこともある。
os.file.Seek(offset, whence)
Seek は、次のファイルへの Read または Write のオフセットを offset に設定し、whence に従って解釈する。
- whence の値の意味
- 0:ファイルの原点からの相対的な移動
- 1:現在のオフセットからの移動
- 2:終点からの移動
新しいオフセットと、もしあればエラーが返される。
O_APPEND でオープンされたファイルに対する Seek の動作は指定されていない。
file がディレクトリの場合、Seekの動作は OS によって異なる。Unix 系 OS では、ディレクトリの先頭までシークできるが、Windows ではできない。
補足
whence: 「どこから」を意味する疑問副詞
あーーー 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)
}