メインコンテンツまでスキップ

標準ライブラリ概観

.NET (C#) の経験がある開発者向けに、fmt、io、encoding/json、time、os など主要な標準パッケージを紹介します。Go の標準ライブラリは小さな部品を組み合わせる設計で、特に io.Reader / io.Writer を中心とした抽象が、ファイル、ネットワーク、圧縮、ハッシュなどあらゆる場所でつながっています。BCL (Base Class Library) のように巨大なクラス階層を持たない代わりに、少数のインターフェースで多くのことを済ませます。HTTP サーバー、JSON、テスト、構造化ログまで標準で揃っているため、「まず標準で書いてみる」が Go の基本姿勢です。

1. このドキュメントで扱うこと​

  • io.Reader / io.Writer の抽象
  • encoding/json とタグ
  • time パッケージの注意点
  • 「標準ライブラリで足りるか」の判断基準

io.Reader / io.Writer の抽象​

Go の入出力は、次の 2 つの小さなインターフェースが基本です。

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

type Writer interface {
Write(p []byte) (n int, err error)
}
  • ファイル (*os.File)、ネットワーク接続、HTTP のリクエスト / レスポンスボディ、strings.Reader、bytes.Buffer、圧縮・暗号化・ハッシュなどが、これらを実装しています。
  • C# の Stream は読み書きとシークを 1 つの抽象クラスにまとめていますが、Go は役割ごとに分けます。io.Closer、io.Seeker、io.ReadWriter のように、必要な組み合わせをインターフェースの埋め込みで作ります。
  • Read は、データの末尾に達すると io.EOF を返します。io.EOF はエラー値ですが、異常ではなく正常な終端を表します。
  • よく使う補助関数は、io.Copy (Reader から Writer へコピー)、io.ReadAll (全体を読み込み)、io.TeeReader、io.MultiWriter、io.LimitReader、bufio.Scanner / bufio.Reader (行や区切り単位の読み取り) などです。
  • 関数の引数は、具体型 (*os.File) ではなく io.Reader / io.Writer で受けると、テストで strings.NewReader や bytes.Buffer を差し込めます。

encoding/json とタグ​

encoding/json は、構造体と JSON の相互変換を行います。System.Text.Json の属性にあたるものが、構造体タグです。

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Password string `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
タグ意味
json:"name"JSON のキー名を指定する
json:"name,omitempty"値がゼロ値 (空文字列、0、nil、空のスライスなど) の場合に出力を省略する
json:"name,omitzero"ゼロ値の場合に省略する (Go 1.24 以降。time.Time のような構造体のゼロ値にも有効)
json:"-"シリアライズの対象から外す
json:",string"数値や真偽値を JSON の文字列として出力する
  • 公開されていないフィールド (小文字始まり) は、エンコード / デコードの対象になりません。
  • 主な API は、json.Marshal / json.Unmarshal (メモリ上のバイト列)、json.NewEncoder / json.NewDecoder (ストリーム) です。HTTP ボディやファイルには、Encoder / Decoder を使います。
  • 型がわからない JSON は any に読み込むと、オブジェクトが map[string]any、数値が float64 になります。
  • json.RawMessage を使うと、一部のフィールドの解釈を後回しにできます。独自の形式は MarshalJSON / UnmarshalJSON メソッドで定義します。

time パッケージの注意点​

time.Time は時刻、time.Duration は時間の長さを表します。DateTime / TimeSpan に近い役割ですが、いくつかの違いがあります。

  • 書式は yyyy-MM-dd のような記号ではなく、基準時刻 Mon Jan 2 15:04:05 MST 2006 を使って書きます。2006-01-02 15:04:05 が yyyy-MM-dd HH:mm:ss に相当します。
  • よく使う書式は、time.RFC3339、time.DateOnly (Go 1.20 以降)、time.DateTime (Go 1.20 以降) などの定数として定義されています。
  • time.Duration は int64 のナノ秒数で、2 * time.Second のように定数との掛け算で作ります。time.Duration(n) * time.Second のように、変数との掛け算では型変換が必要です。
  • time.Time は、タイムゾーン (Location) を内部に持ち、time.Now() はローカルのタイムゾーンで返ります。保存や比較では UTC() にそろえるのが安全です。
  • 比較は == ではなく、t.Equal(u)、t.Before(u)、t.After(u) を使います。== は、タイムゾーンや monotonic clock の情報まで比較してしまうためです。
  • ゼロ値の time.Time{} は、0001-01-01 00:00:00 UTC です。t.IsZero() で判定します。
  • 経過時間の計測は time.Since(start) を使います。time.Now() には単調増加クロックが含まれるため、壁時計が変更されても正しく計測できます。

「標準ライブラリで足りるか」の判断基準​

Go では、サードパーティーのライブラリを足す前に、まず標準で書けないかを確認するのが一般的です。

やりたいこと標準外部を検討するケース
HTTP サーバー / クライアントnet/httpルートのグルーピングが多い (chi など)
JSONencoding/json非常に高い性能が必要 (高速な JSON ライブラリ)
構造化ログlog/slog (Go 1.21 以降)既存のログ基盤との連携 (zap、zerolog など)
テストtesting、net/http/httptestアサーションの簡略化 (testify)、モック生成
コマンドライン引数flagサブコマンドが多い CLI (cobra など)
設定ファイル / 環境変数os.Getenv、encoding/jsonYAML / TOML、階層的な設定 (viper など)
コレクション操作slices、maps (Go 1.21 以降)特殊なデータ構造
データベースdatabase/sqlORM やコード生成 (sqlc など)

判断の目安は次のとおりです。

  • 標準で数十行書ける程度なら、依存を増やさない方が長期的に保守しやすくなります。
  • 標準が提供しているのは「基本的で互換性が保証された部品」です。Go 1 の互換性の約束により、標準のコードは長期にわたって動作し続けます。
  • 外部ライブラリは、メンテナンスの状況、依存の数、ライセンスを確認してから採用します。

2. .NET との対応表​

.NET (C#)Go
System.Text.Jsonencoding/json
System.IO.Streamio.Reader / io.Writer
DateTime / TimeSpantime.Time / time.Duration
Console / String.Formatfmt

上の表を補う対応関係です。

.NET (C#)Go
File.ReadAllText / File.WriteAllTextos.ReadFile / os.WriteFile
StreamReader.ReadLinebufio.Scanner / bufio.Reader
Stream.CopyToio.Copy
MemoryStreambytes.Buffer / bytes.Reader
StringBuilderstrings.Builder
string.Split / Join / Containsstrings.Split / strings.Join / strings.Contains
int.Parse / int.TryParsestrconv.Atoi / strconv.ParseInt (エラーを戻り値で返す)
Environment.GetEnvironmentVariableos.Getenv / os.LookupEnv
Environment.GetCommandLineArgsos.Args / flag パッケージ
Path.Combinepath/filepath.Join
Regexregexp (RE2 構文。後方参照や先読みは使えない)
List<T>.Sort / LINQ の OrderByslices.Sort / slices.SortFunc
Dictionary<TKey, TValue>.Keysmaps.Keys (イテレーターを返す。Go 1.23 以降)
Stopwatchtime.Now() と time.Since
Task.Delay / PeriodicTimertime.After / time.NewTimer / time.NewTicker
ILoggerlog/slog
Math.Max / Math.Min組み込みの max / min (Go 1.21 以降)
Guid.NewGuid()標準にはなし (crypto/rand で生成するか、外部ライブラリを使う)
xUnit / NUnittesting

3. C# 開発者がつまずきやすいポイント​

Stream と違い、Reader / Writer は小さく分かれている​

読むだけなら io.Reader、書くだけなら io.Writer を受け取ります。シークが必要な場合に限って io.Seeker や io.ReaderAt を足します。必要最小限のインターフェースを引数に取るのが慣習です。また、Read は要求したバイト数より少なく返すことがあるため、全体を読むには io.ReadAll や io.ReadFull、ループを使います。

Close を忘れない​

using のような仕組みはありません。os.Open や HTTP の resp.Body などは、取得の直後に defer f.Close() と書きます。書き込み用のファイルでは、Close が返すエラー (バッファのフラッシュに失敗した場合など) も確認する必要があります。

JSON のフィールド名は、タグがなければ構造体の名前のまま​

System.Text.Json の JsonNamingPolicy.CamelCase のようなグローバル設定はありません。タグを付けなければ、Name というフィールド名のまま ("Name") 出力されます。デコード時は、大文字小文字を区別せずに一致するフィールドを探します。

ゼロ値と「未指定」を区別できない​

omitempty は、0、空文字列、false なども省略します。そのため、「値が 0 である」と「値が指定されていない」を区別したい場合は、ポインター (*int) を使います。デコード時も、フィールドが JSON になかったのか、0 が入っていたのかの区別が付きません。

未知のフィールドは、デフォルトでは無視される​

デコードの対象にない JSON キーは、黙って無視されます。厳密にしたい場合は、json.Decoder の DisallowUnknownFields を使います。

数値は any に読み込むと float64 になる​

型を指定せずに any や map[string]any に読み込むと、数値はすべて float64 になります。大きな整数が精度を失うことがあるため、構造体を定義するか、Decoder.UseNumber を使います。

time.Time の書式は特別な基準日で書く​

time.Now().Format("yyyy-MM-dd") は、期待通りに動きません。"2006-01-02" のように、基準時刻の各要素 (年は 2006、月は 01、日は 02、時は 15、分は 04、秒は 05) を使います。go vet の timeformat チェックは、2006-02-01 のように年月日の並びを取り違えた書式を検出します。

time.Duration の型変換​

time.Sleep(n * time.Second) は、n が int 型だとコンパイルエラーになります。time.Duration(n) * time.Second と書きます。定数 (2 * time.Second) は型なし定数なので、そのまま使えます。

文字列は UTF-8 のバイト列​

len(s) はバイト数で、文字数ではありません。for i, r := range s は rune (Unicode コードポイント) 単位で走査し、i はバイト位置です。日本語の文字数を数えるには、utf8.RuneCountInString(s) か []rune(s) を使います。

fmt.Println と slog の使い分け​

fmt は書式付きの出力に使い、サービスの運用ログには、log/slog で構造化ログを出力します。slog は、キーと値のペアで属性を渡し、JSON などの形式で出力できます。

標準の log と slog​

log パッケージは古い API で、レベルや構造化の機能がありません。新しいコードでは、slog を選びます。

4. コード例​

例 1: io.Reader / io.Writer を組み合わせる​

C#:

using var input = File.OpenRead("input.txt");
using var output = File.Create("copy.txt");
using var sha = SHA256.Create();

// コピーしながらハッシュを計算する場合は、手動でバッファーを読み書きする
var buffer = new byte[8192];
int n;
while ((n = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, n);
sha.TransformBlock(buffer, 0, n, null, 0);
}
sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
Console.WriteLine(Convert.ToHexString(sha.Hash!));

Go では、io.MultiWriter を使って、複数の Writer へ同時に書き込めます。

package main

import (
"crypto/sha256"
"fmt"
"io"
"os"
)

func copyWithHash(dstPath, srcPath string) (string, error) {
src, err := os.Open(srcPath)
if err != nil {
return "", err
}
defer src.Close()

dst, err := os.Create(dstPath)
if err != nil {
return "", err
}

h := sha256.New() // hash.Hash は io.Writer を実装している

// dst と h の両方に書き込む Writer を作り、io.Copy でコピーする
if _, err := io.Copy(io.MultiWriter(dst, h), src); err != nil {
dst.Close()
return "", err
}

// 書き込みのエラーを取りこぼさないよう、Close のエラーも返す
if err := dst.Close(); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

func main() {
sum, err := copyWithHash("copy.txt", "input.txt")
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println(sum)
}

次は、bufio.Scanner で行ごとに読み込む例です。strings.NewReader を使うと、ファイルなしでも同じコードを試せます。

package main

import (
"bufio"
"fmt"
"io"
"strings"
)

func countLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
n := 0
for sc.Scan() {
fmt.Println(sc.Text())
n++
}
return n, sc.Err() // 読み取り中のエラーは Err で確認する
}

func main() {
n, err := countLines(strings.NewReader("one\ntwo\nthree\n"))
fmt.Println(n, err)
}

countLines は io.Reader を受け取るため、ファイルでも HTTP ボディでも標準入力 (os.Stdin) でも、そのまま使えます。

例 2: encoding/json の基本​

C# (System.Text.Json):

public record User(
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("email")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Email);

var json = JsonSerializer.Serialize(new User(1, "Alice", null));
var user = JsonSerializer.Deserialize<User>(json);

Go:

package main

import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
)

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"` // nil なら出力しない
Password string `json:"-"` // 出力しない
CreatedAt time.Time `json:"created_at"`
}

func main() {
u := User{
ID: 1,
Name: "Alice",
Password: "secret",
CreatedAt: time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC),
}

// 構造体 -> JSON
b, err := json.MarshalIndent(u, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(b))

// JSON -> 構造体 (Decoder で未知のフィールドを拒否する)
in := `{"id": 2, "name": "Bob", "created_at": "2025-03-04T05:06:07Z", "extra": true}`
dec := json.NewDecoder(strings.NewReader(in))
dec.DisallowUnknownFields()

var got User
if err := dec.Decode(&got); err != nil {
fmt.Println("decode error:", err) // extra が未知のフィールドなのでエラー
}

// 型を決めずに読み込む場合、数値は float64 になる
var m map[string]any
if err := json.Unmarshal([]byte(`{"n": 1}`), &m); err != nil {
panic(err)
}
fmt.Printf("%T\n", m["n"]) // float64

// ストリームへの書き出し (HTTP レスポンスやファイルにも使える)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(map[string]int{"count": 3})
}

json.Marshal はエラーを返します。チャネルや関数など、JSON にできない型を含む場合や、MarshalJSON がエラーを返した場合などに発生します。

例 3: time パッケージ​

C#:

var sw = Stopwatch.StartNew();
var timeout = TimeSpan.FromSeconds(30);
var t = DateTime.ParseExact("2025-01-02", "yyyy-MM-dd", CultureInfo.InvariantCulture);
Console.WriteLine(t.AddDays(7).ToString("yyyy-MM-dd HH:mm:ss"));

Go:

package main

import (
"fmt"
"time"
)

func main() {
start := time.Now()
timeout := 30 * time.Second

// パース: レイアウトは基準時刻 2006-01-02 15:04:05 で書く
t, err := time.Parse("2006-01-02", "2025-01-02")
if err != nil {
panic(err)
}

// 加算とフォーマット
next := t.AddDate(0, 0, 7)
fmt.Println(next.Format("2006-01-02 15:04:05"))
fmt.Println(next.Format(time.RFC3339))

// 比較は Equal / Before / After を使う
fmt.Println(next.After(t), next.Equal(t))

// 時間の長さ
d := 90 * time.Minute
fmt.Println(d, d.Hours()) // 1h30m0s 1.5

n := 3
wait := time.Duration(n) * time.Second // 変数との掛け算は型変換が必要
fmt.Println(wait, timeout)

// ゼロ値の判定
var zero time.Time
fmt.Println(zero.IsZero())

// 経過時間の計測
fmt.Println(time.Since(start) < time.Second)

// タイムゾーンの扱い
jst := time.FixedZone("JST", 9*60*60)
fmt.Println(time.Date(2025, 1, 2, 9, 0, 0, 0, jst).UTC())
}

time.LoadLocation("Asia/Tokyo") で IANA のタイムゾーンデータベースを使うこともできます。実行環境にタイムゾーンデータがない場合 (最小のコンテナイメージなど) は、time/tzdata パッケージを空の import (import _ "time/tzdata") で組み込むと、バイナリに埋め込めます。

例 4: fmt、errors、os、flag​

package main

import (
"errors"
"flag"
"fmt"
"io/fs"
"os"
)

type Point struct {
X, Y int
}

func main() {
// fmt の動詞
p := Point{1, 2}
fmt.Printf("%v\n", p) // {1 2}
fmt.Printf("%+v\n", p) // {X:1 Y:2}
fmt.Printf("%#v\n", p) // main.Point{X:1, Y:2}
fmt.Printf("%T\n", p) // main.Point
fmt.Printf("%q\n", "hello") // "hello"

// コマンドライン引数
name := flag.String("name", "world", "name to greet")
verbose := flag.Bool("v", false, "verbose output")
flag.Parse()
fmt.Printf("hello, %s (verbose=%v)\n", *name, *verbose)

// 環境変数: 未設定と空文字列を区別したいときは LookupEnv を使う
if v, ok := os.LookupEnv("APP_ENV"); ok {
fmt.Println("APP_ENV =", v)
}

// ファイル読み込みとエラーの判定
data, err := os.ReadFile("config.json")
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("config.json がありません。デフォルト設定を使います")
} else if err != nil {
fmt.Fprintln(os.Stderr, "読み込みに失敗しました:", err)
os.Exit(1)
} else {
fmt.Println(len(data), "bytes")
}

// エラーのラップ (%w)
wrapped := fmt.Errorf("load config: %w", fs.ErrNotExist)
fmt.Println(wrapped, errors.Is(wrapped, fs.ErrNotExist))
}
  • fmt.Errorf の %w でエラーをラップすると、errors.Is や errors.As で、元のエラーを判定できます。
  • 診断メッセージは os.Stderr に出力し、異常終了では os.Exit(1) のようにステータスコードを返します。

例 5: log/slog、slices、maps​

C#:

logger.LogInformation("user {UserId} logged in from {Ip}", userId, ip);

var names = users.Select(u => u.Name).OrderBy(n => n).ToList();
var keys = dict.Keys.OrderBy(k => k).ToList();

Go:

package main

import (
"cmp"
"log/slog"
"maps"
"os"
"slices"
"strings"
)

type User struct {
Name string
Age int
}

func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)

// キーと値のペアで属性を渡す
slog.Info("user logged in", "user_id", 42, "ip", "192.0.2.1")
slog.Debug("cache miss", "key", "user:42")

// 属性を固定した子ロガー
reqLogger := logger.With("request_id", "req-1")
reqLogger.Warn("slow query", "ms", 1200)

users := []User{{"Carol", 30}, {"Alice", 25}, {"Bob", 25}}

// 年齢、次に名前の順で並べ替える (LINQ の OrderBy().ThenBy() に近い)
slices.SortFunc(users, func(a, b User) int {
return cmp.Or(
cmp.Compare(a.Age, b.Age),
strings.Compare(a.Name, b.Name),
)
})
slog.Info("sorted", "users", users)

// 含まれるかの判定
names := []string{"Alice", "Bob"}
slog.Info("contains", "result", slices.Contains(names, "Bob"))

// マップのキーを並べ替えたスライスに変換する (Go 1.23 以降)
m := map[string]int{"b": 2, "a": 1, "c": 3}
keys := slices.Sorted(maps.Keys(m))
slog.Info("keys", "keys", keys)
}
  • slog は、slog.Info("メッセージ", "キー", 値, ...) の形式で、キーと値を交互に渡します。
  • cmp.Or は Go 1.22 以降で使えます。最初にゼロ値でない値を返すため、複数のキーでの並べ替えを簡潔に書けます。
  • マップの走査順序は不定です。出力を安定させたい場合は、キーを取り出して並べ替えます。

5. まとめ​

  • 標準ライブラリは、少数のインターフェースを中心に小さな部品を組み合わせる設計です。特に io.Reader / io.Writer は、ファイル、ネットワーク、HTTP、圧縮、ハッシュをつなぐ共通の抽象です。
  • 関数の引数は、具体型ではなく必要最小限のインターフェースで受けると、テストしやすく再利用しやすくなります。
  • encoding/json は構造体タグで制御します。公開フィールドのみが対象で、未知のフィールドは無視され、any に読み込んだ数値は float64 になります。
  • time は、基準時刻 2006-01-02 15:04:05 で書式を書き、比較には Equal / Before / After を使います。Duration と変数の掛け算では型変換が必要です。
  • defer で Close を呼び、エラーは戻り値で扱い、%w でラップして errors.Is / errors.As で判定します。
  • ログには log/slog、コレクション操作には slices / maps など、新しい標準パッケージを優先して使います。
  • サードパーティーを足す前に、まず標準で足りるかを確認します。標準は Go 1 の互換性の約束により、長期的に安定しています。

参考資料​