課題10: インターフェースを使ったI/Oシステム

課題概要

インターフェースを使って、I/Oシステムを実装します。io.Reader、io.Writer、カスタムインターフェース、デコレーターパターン、モックを学びます。

マンダトリー要件

要件 1: カスタムReader実装

様々なReaderを実装してください。

ファイル: reader/custom_readers.go

package reader

import (
    "io"
)

// StringReader は文字列から読み込む
type StringReader struct {
    data string
    pos  int
}

// NewStringReader は StringReader を作成
func NewStringReader(data string) *StringReader {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (sr *StringReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - data[pos:] から p にコピー
    // - pos を更新
    // - すべて読み終わったら io.EOF を返す
}

// LimitReader は最大バイト数まで読み込む
type LimitReader struct {
    reader io.Reader
    limit  int64
    read   int64
}

// NewLimitReader は LimitReader を作成
func NewLimitReader(r io.Reader, limit int64) *LimitReader {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (lr *LimitReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - limit まで読み込んだら io.EOF
    // - 一度に読み込むサイズを制限
}

// RepeatReader は同じデータを繰り返し読み込む
type RepeatReader struct {
    data  []byte
    count int  // 残り繰り返し回数(-1は無限)
    pos   int
}

// NewRepeatReader は RepeatReader を作成
func NewRepeatReader(data []byte, count int) *RepeatReader {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (rr *RepeatReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - count 回繰り返す(-1なら無限)
}

// MultiReader は複数のReaderを順番に読み込む
type MultiReader struct {
    readers []io.Reader
    current int
}

// NewMultiReader は MultiReader を作成
func NewMultiReader(readers ...io.Reader) *MultiReader {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (mr *MultiReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - 現在のReaderから読み込み
    // - EOFなら次のReaderに移動
}

要件2: カスタムWriter実装

様々なWriterを実装してください。

ファイル: writer/custom_writers.go

package writer

import (
    "io"
)

// CountWriter は書き込みバイト数をカウント
type CountWriter struct {
    writer io.Writer
    count  int64
}

// NewCountWriter は CountWriter を作成
func NewCountWriter(w io.Writer) *CountWriter {
    // TODO: 実装してください
}

// Write は io.Writer を実装
func (cw *CountWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - writer に書き込み
    // - count を更新
}

// Count は書き込み総バイト数を返す
func (cw *CountWriter) Count() int64 {
    // TODO: 実装してください
}

// PrefixWriter は各行にプレフィックスを追加
type PrefixWriter struct {
    writer      io.Writer
    prefix      string
    atLineStart bool
}

// NewPrefixWriter は PrefixWriter を作成
func NewPrefixWriter(w io.Writer, prefix string) *PrefixWriter {
    // TODO: 実装してください
}

// Write は io.Writer を実装
func (pw *PrefixWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - 行の先頭にプレフィックスを追加
    // - 改行を検出して次の行もプレフィックス付き
}

// TeeWriter は2つのWriterに同時に書き込む
type TeeWriter struct {
    writer1 io.Writer
    writer2 io.Writer
}

// NewTeeWriter は TeeWriter を作成
func NewTeeWriter(w1, w2 io.Writer) *TeeWriter {
    // TODO: 実装してください
}

// Write は io.Writer を実装
func (tw *TeeWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
    // - 両方のWriterに書き込む
    // - どちらかがエラーならエラーを返す
}

// MultiWriter は複数のWriterに同時に書き込む
type MultiWriter struct {
    writers []io.Writer
}

// NewMultiWriter は MultiWriter を作成
func NewMultiWriter(writers ...io.Writer) *MultiWriter {
    // TODO: 実装してください
}

// Write は io.Writer を実装
func (mw *MultiWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
}

要件3: ストリーム処理パイプライン

ストリーム処理のパイプラインを実装してください。

ファイル: pipeline/pipeline.go

package pipeline

import (
    "io"
)

// Processor はデータ変換インターフェース
type Processor interface {
    Process(input io.Reader, output io.Writer) error
}

// UpperCaseProcessor は大文字に変換
type UpperCaseProcessor struct{}

func (p *UpperCaseProcessor) Process(input io.Reader, output io.Writer) error {
    // TODO: 実装してください
}

// LowerCaseProcessor は小文字に変換
type LowerCaseProcessor struct{}

func (p *LowerCaseProcessor) Process(input io.Reader, output io.Writer) error {
    // TODO: 実装してください
}

// ReverseProcessor は行を逆順にする
type ReverseProcessor struct{}

func (p *ReverseProcessor) Process(input io.Reader, output io.Writer) error {
    // TODO: 実装してください
}

// FilterProcessor は条件に合う行のみ出力
type FilterProcessor struct {
    predicate func(string) bool
}

func NewFilterProcessor(predicate func(string) bool) *FilterProcessor {
    // TODO: 実装してください
}

func (p *FilterProcessor) Process(input io.Reader, output io.Writer) error {
    // TODO: 実装してください
}

// Pipeline はプロセッサーのパイプライン
type Pipeline struct {
    processors []Processor
}

// NewPipeline は Pipeline を作成
func NewPipeline() *Pipeline {
    // TODO: 実装してください
}

// Add はプロセッサーを追加
func (p *Pipeline) Add(processor Processor) *Pipeline {
    // TODO: 実装してください
}

// Execute はパイプラインを実行
func (p *Pipeline) Execute(input io.Reader, output io.Writer) error {
    // TODO: 実装してください
    // - 各プロセッサーを順番に実行
    // - 中間結果は bytes.Buffer を使う
}

要件4: ファイル処理ツール

実用的なファイル処理ツールを実装してください。

ファイル: filetools/filetools.go

package filetools

import (
    "io"
)

// Copy はファイルをコピー
func Copy(src, dst string) error {
    // TODO: 実装してください
    // - os.Open で src を開く
    // - os.Create で dst を作成
    // - io.Copy でコピー
    // - defer でクローズ
}

// CopyWithProgress は進捗を表示してコピー
func CopyWithProgress(src, dst string, progress func(copied, total int64)) error {
    // TODO: 実装してください
    // - ファイルサイズを取得
    // - コピーしながら progress を呼ぶ
}

// Cat は複数ファイルを連結
func Cat(output string, inputs ...string) error {
    // TODO: 実装してください
    // - output ファイルを作成
    // - 各 input ファイルを順番にコピー
}

// Split はファイルを複数に分割
func Split(input string, chunkSize int64) ([]string, error) {
    // TODO: 実装してください
    // - chunkSize バイトごとに分割
    // - input.part001, input.part002, ... というファイル名
    // - 作成したファイル名のスライスを返す
}

// Grep は正規表現にマッチする行を抽出
func Grep(pattern, filename string) ([]string, error) {
    // TODO: 実装してください
    // - regexp.MustCompile でパターンをコンパイル
    // - 各行をチェック
}

// LineCount はファイルの行数を返す
func LineCount(filename string) (int, error) {
    // TODO: 実装してください
}

// Head は最初のn行を返す
func Head(filename string, n int) ([]string, error) {
    // TODO: 実装してください
}

// Tail は最後のn行を返す
func Tail(filename string, n int) ([]string, error) {
    // TODO: 実装してください
}

期待される出力

// StringReader
reader := NewStringReader("Hello, World!")
buf := make([]byte, 5)
n, _ := reader.Read(buf)
fmt.Println(string(buf[:n]))  // "Hello"

// CountWriter
var buffer bytes.Buffer
cw := NewCountWriter(&buffer)
cw.Write([]byte("Hello"))
cw.Write([]byte("World"))
fmt.Println(cw.Count())  // 10

// Pipeline
pipeline := NewPipeline().
    Add(&UpperCaseProcessor{}).
    Add(NewFilterProcessor(func(line string) bool {
        return len(line) > 5
    }))

input := strings.NewReader("hello\nworld\ngo")
var output bytes.Buffer
pipeline.Execute(input, &output)
fmt.Println(output.String())  // "HELLO\nWORLD\n"

// ファイルコピー
err := Copy("source.txt", "dest.txt")

// 進捗付きコピー
err = CopyWithProgress("large.dat", "copy.dat", func(copied, total int64) {
    fmt.Printf("\rProgress: %d/%d (%.1f%%)",
        copied, total, float64(copied)/float64(total)*100)
})

ボーナス課題

ボーナス1: 圧縮/解凍Reader/Writer

package compress

import (
    "compress/gzip"
    "io"
)

// CompressWriter は gzip 圧縮して書き込む
type CompressWriter struct {
    writer     io.Writer
    gzipWriter *gzip.Writer
}

// NewCompressWriter は CompressWriter を作成
func NewCompressWriter(w io.Writer) *CompressWriter {
    // TODO: 実装してください
}

// Write は io.Writer を実装
func (cw *CompressWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
}

// Close はリソースを解放
func (cw *CompressWriter) Close() error {
    // TODO: 実装してください
}

// DecompressReader は gzip 解凍して読み込む
type DecompressReader struct {
    reader     io.Reader
    gzipReader *gzip.Reader
}

// NewDecompressReader は DecompressReader を作成
func NewDecompressReader(r io.Reader) (*DecompressReader, error) {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (dr *DecompressReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
}

// Close はリソースを解放
func (dr *DecompressReader) Close() error {
    // TODO: 実装してください
}

ボーナス2: 暗号化Reader/Writer

package crypto

import (
    "crypto/aes"
    "crypto/cipher"
    "io"
)

// EncryptWriter は暗号化して書き込む
type EncryptWriter struct {
    writer io.Writer
    stream cipher.Stream
}

// NewEncryptWriter は EncryptWriter を作成
func NewEncryptWriter(w io.Writer, key []byte) (*EncryptWriter, error) {
    // TODO: 実装してください
    // - AES暗号を初期化
    // - CTRモードでストリーム暗号を作成
}

// Write は io.Writer を実装
func (ew *EncryptWriter) Write(p []byte) (n int, err error) {
    // TODO: 実装してください
}

// DecryptReader は復号化して読み込む
type DecryptReader struct {
    reader io.Reader
    stream cipher.Stream
}

// NewDecryptReader は DecryptReader を作成
func NewDecryptReader(r io.Reader, key []byte) (*DecryptReader, error) {
    // TODO: 実装してください
}

// Read は io.Reader を実装
func (dr *DecryptReader) Read(p []byte) (n int, err error) {
    // TODO: 実装してください
}

ボーナス3: HTTP ストリーミング

package httpstream

import (
    "io"
    "net/http"
)

// StreamDownload はHTTPからストリーミングダウンロード
func StreamDownload(url, filename string, progress func(downloaded, total int64)) error {
    // TODO: 実装してください
    // - http.Get でダウンロード開始
    // - Content-Length ヘッダーから総サイズ取得
    // - io.Copy でストリーミング保存
    // - 定期的に progress を呼ぶ
}

// StreamUpload はHTTPにストリーミングアップロード
func StreamUpload(url, filename string, progress func(uploaded, total int64)) error {
    // TODO: 実装してください
}

// Proxy は HTTP リクエストをプロキシ
func Proxy(target string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO: 実装してください
        // - target にリクエストを転送
        // - レスポンスをストリーミングで返す
    }
}

評価基準

項目 配点 詳細
カスタムReader 25点 各Readerが正しく実装されている
カスタムWriter 25点 各Writerが正しく実装されている
パイプライン 25点 ストリーム処理が動作する
ファイルツール 25点 実用的なツールが実装されている
**ボーナス1** 10点 圧縮/解凍が実装されている
**ボーナス2** 10点 暗号化/復号化が実装されている
**ボーナス3** 5点 HTTPストリーミングが実装されている

提出方法

submission/
├── go.mod
├── reader/
│   ├── custom_readers.go
│   └── custom_readers_test.go
├── writer/
│   ├── custom_writers.go
│   └── custom_writers_test.go
├── pipeline/
│   ├── pipeline.go
│   └── pipeline_test.go
├── filetools/
│   ├── filetools.go
│   └── filetools_test.go
└── bonus/
    ├── compress/
    ├── crypto/
    └── httpstream/

ヒント

  • Reader: bufio.Readerを使うと便利
  • Writer: bytes.Bufferで中間バッファ
  • Pipeline: io.Pipeでストリーミング接続
  • コピー: io.Copyを活用
  • 進捗: io.TeeReaderでバイト数をカウント
  • 暗号化: crypto/aes と crypto/cipher を使う
  • テストケース

    // reader/custom_readers_test.go
    func TestStringReader(t *testing.T) {
        reader := NewStringReader("Hello")
        buf := make([]byte, 3)
    
        n, err := reader.Read(buf)
        if n != 3 || err != nil {
            t.Errorf("Read() = %d, %v; want 3, nil", n, err)
        }
        if string(buf) != "Hel" {
            t.Errorf("Read data = %q; want %q", string(buf), "Hel")
        }
    
        n, err = reader.Read(buf)
        if n != 2 || err != io.EOF {
            t.Errorf("Read() = %d, %v; want 2, EOF", n, err)
        }
    }
    
    // writer/custom_writers_test.go
    func TestCountWriter(t *testing.T) {
        var buf bytes.Buffer
        cw := NewCountWriter(&buf)
    
        cw.Write([]byte("Hello"))
        cw.Write([]byte("World"))
    
        if cw.Count() != 10 {
            t.Errorf("Count() = %d; want 10", cw.Count())
        }
        if buf.String() != "HelloWorld" {
            t.Errorf("Written data = %q; want %q", buf.String(), "HelloWorld")
        }
    }
    
    // pipeline/pipeline_test.go
    func TestPipeline(t *testing.T) {
        input := strings.NewReader("hello\nworld")
        var output bytes.Buffer
    
        pipeline := NewPipeline().
            Add(&UpperCaseProcessor{})
    
        err := pipeline.Execute(input, &output)
        if err != nil {
            t.Fatalf("Execute() error = %v", err)
        }
    
        expected := "HELLO\nWORLD"
        if output.String() != expected {
            t.Errorf("Output = %q; want %q", output.String(), expected)
        }
    }
    

    学習リソース

  • Go by Example - Reading Files
  • Go by Example - Writing Files
  • io package
  • bufio package
  • Effective Go - Interfaces

まとめ

この課題を完了すると、以下ができるようになります:

  • io.Reader/Writerインターフェースの実装
  • デコレーターパターンの活用
  • ストリーム処理のパイプライン構築
  • 実用的なファイル処理ツールの作成

Go Foundations コース完了おめでとうございます!