Go
AdventCalendar
golang
Go3Day 2

標準パッケージから見るパッケージ構成のパターンの解説

こんにちは!これはGo3 Advent Calendar 2018、2日目の記事です。
1日目は、higasgtさんのredisを扱うコードをユニットテストするでした。

TL;DR

  • Goのパッケージ構成は、標準パッケージでも複数パターンの構成が存在している
  • 習う際は、いくつかのパッケージ構造を比較してどのように実装するかを決めるのが良い
  • io と database/sql は有名な標準パッケージだがパッケージ構成のアプローチは異なる

ioパッケージの構成について

ioパッケージは io.Reader io.Writer をはじめとして有名なパッケージです。
Goを触っている人は ioutil.ReadAll() にお世話になった人も多いのではないでしょうか。

io
├── example_test.go
├── io.go
├── io_test.go
├── ioutil
│   ├── example_test.go
│   ├── ioutil.go
│   ├── ioutil_test.go
│   ├── tempfile.go
│   ├── tempfile_test.go
│   └── testdata
│       └── hello
├── multi.go
├── multi_test.go
├── pipe.go
└── pipe_test.go

ioパッケージを構成するディレクトリ構造は上記のようになっています。
1つづつ解説していきます。

io.go

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

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

io.go は Reader Writer Closer をはじめとする抽象を実装しています。
パッケージのコメントに書いてある通りです。

Package io provides basic interfaces to I/O primitives.
Its primary job is to wrap existing implementations of such primitives, such as those in package os, into shared public interfaces that abstract the functionality, plus some other related primitives.

ioutil/ioutil.go

ioutilパッケージについては、下記のコメントのようにいくつかのutilityを実装しています。

Package ioutil implements some I/O utility functions.

具体的な実装として呼び出す際は下記のように ioutil の具象実装を利用します

r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")

b, err := ioutil.ReadAll(r)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("%s", b)

なので、パッケージ構成としては下記のように、ioが抽象 / io/ioutilが具象となっています。

io // 抽象
├── example_test.go
├── io.go
├── io_test.go
├── ioutil // 具象
│   ├── example_test.go
│   ├── ioutil.go
│   ├── ioutil_test.go
│   ├── tempfile.go
│   ├── tempfile_test.go
│   └── testdata
│       └── hello
├── multi.go
├── multi_test.go
├── pipe.go
└── pipe_test.go

database/sql

database/sqlパッケージについてもioの時と同様に追いかけていきましょう。

sql/
├── convert.go
├── convert_test.go
├── ctxutil.go
├── doc.txt
├── driver
│   ├── driver.go
│   ├── types.go
│   └── types_test.go
├── example_test.go
├── fakedb_test.go
├── sql.go
└── sql_test.go

パッケージ構成は上記のようになっています。

sql.go

func Open(driverName, dataSourceName string) (*DB, error) {
    driversMu.RLock()
    driveri, ok := drivers[driverName]
    driversMu.RUnlock()
    if !ok {
        return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
    }

    if driverCtx, ok := driveri.(driver.DriverContext); ok {
        connector, err := driverCtx.OpenConnector(dataSourceName)
        if err != nil {
            return nil, err
        }
        return OpenDB(connector), nil
    }

    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

sql.goは上記のように、 driveri, ok := drivers[driverName] という行で Driver を取り出して処理します。
Driver は ioの時とは逆に、driver ディレクトリの中に実装されており、 Driver インターフェースなど抽象実装がされています

type Driver interface {
    // Open returns a new connection to the database.
    // The name is a string in a driver-specific format.
    //
    // Open may return a cached connection (one previously
    // closed), but doing so is unnecessary; the sql package
    // maintains a pool of idle connections for efficient re-use.
    //
    // The returned connection is only used by one goroutine at a
    // time.
    Open(name string) (Conn, error)
}

go-sql-driver/driver.go

このDriverを使用しているのが go-sql-driverパッケージです。
以下のように init() 関数でDriverを登録し、その後の処理で利用しています。

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

パッケージの構成がioとは逆で、以下のようになります。

sql/ // 具象
├── convert.go
├── convert_test.go
├── ctxutil.go
├── doc.txt
├── driver // 抽象
│   ├── driver.go
│   ├── types.go
│   └── types_test.go
├── example_test.go
├── fakedb_test.go
├── sql.go
└── sql_test.go

まとめ

  • パッケージの構成は標準パッケージでも異なる
  • 自分の実装の参考にする際は、複数のパッケージ構成を見比べた上でメリットがある方をえらぶとよい

おわりに

今日は標準パッケージの読み方を紹介しました。
明日は m_green14 さんです!