以下の内容はhttps://devlights.hatenablog.com/entry/2025/04/18/073000より取得しました。


Goメモ-564 (io.ReadFullとio.ErrUnexpectedEOF)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。使いたいときに忘れているので、ここにメモメモ。。。

io.ReadFullという関数、皆さん使ってますか?

引数に指定したバッファ全部にデータが読み取られるまでブロッキングしてくれる関数です。通信処理と相性が良いです。

通信処理では、固定長ヘッダーと可変長データ部を持つ電文(古い人間なのでメッセージより電文の方がしっくり来ます)を処理したい場合

基本的な処理フローとして、まず最初にヘッダー部を読み取ることになります。大抵はその中にデータ長フィールドがある。

また、先頭Nバイトはシグネチャフィールドとなっている電文もあったりします。その場合、最初にその部分を読み取ったりします。

そういうときは、決まったサイズのデータをきっちり読み取る必要があります。読み取れなかったら、後続の処理に進むことが出来ない。

ですが、TCP/IPはストリーム型の通信プロトコルなので、常に決まった分量が受信出来るわけではありません。最初のrecvでは1バイト分しか読み取れないかもしれないし、一気に1024バイト読み取れるかもしれない。

上記のようなことがあるので、基本的に conn.Read() を行う部分は戻り値に「何バイト読み込めたのか」が返ってきますので、それを確認して必要な情報量を読み取れたかどうかをチェックすることになります。

こういうときに、io.ReadFullさんを利用するととても便利です。

データは受信出来ているけど、指定サイズ分読み取れなかった場合は io.ErrUnexpectedEOF をエラーとして返してくれます。

サンプル

面倒なので、1ファイルでサーバとクライアントを兼用しています。

main.go

package main

import (
    "errors"
    "flag"
    "io"
    "log"
    "net"
    "strings"
    "time"
)

type (
    Args struct {
        bufsize int
        length  int
        timeout time.Duration
    }
)

var (
    args Args
)

func init() {
    flag.IntVar(&args.bufsize, "bufsize", 4, "bufsize")
    flag.IntVar(&args.length, "length", 2, "length")
    flag.DurationVar(&args.timeout, "timeout", 1*time.Second, "timeout")
}

func main() {
    log.SetFlags(log.Lmicroseconds)
    flag.Parse()

    ln, err := net.Listen("tcp", ":8888")
    if err != nil {
        panic(err)
    }
    defer ln.Close()

    go func() {
        conn, err := ln.Accept()
        if err != nil {
            if errors.Is(err, net.ErrClosed) {
                return
            }
            panic(err)
        }
        defer conn.Close()

        buf := []byte(strings.Repeat("h", args.length))
        _, err = conn.Write(buf)
        if err != nil {
            panic(err)
        }

        time.Sleep(500 * time.Millisecond)
    }()

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        return err
    }
    defer conn.Close()

    log.Println("[C] recv start")
    {
        buf := make([]byte, args.bufsize)
        for {
            clear(buf)

            err = conn.SetReadDeadline(time.Now().Add(args.timeout))
            if err != nil {
                return err
            }

            n, err := io.ReadFull(conn, buf)
            if err != nil {
                switch {
                case errors.Is(err, io.EOF):
                    log.Println("io.EOF")
                    return nil
                case errors.Is(err, io.ErrUnexpectedEOF):
                    log.Println("io.ErrUnexpectedEOF")
                    return nil
                default:
                    var netErr net.Error
                    if errors.As(err, &netErr) && netErr.Timeout() {
                        log.Println("netErr.Timeout()")
                        return nil
                    }
                }

                return err
            }

            log.Printf("[C] data=(%s)", buf[:n])
        }
    }
}

Taskfile.yml

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: build
      - task: run
  build:
    cmds:
      - go build -o app .
  run:
    cmds:
      - ./app -timeout 100ms
      - ./app -timeout 3s
      - ./app -length 10 -timeout 3s
      - ./app -bufsize 6 -length 12 -timeout 3s

実行

サーバはクライアントが接続してきたら、起動時引数のlengthバイト分のデータを送信します。

クライアント側は、起動時引数のbufsize分のバッファを作って、io.ReadFullをタイムアウトつきで呼び出し。

起動時引数の指定方法によって、io.EOF, io.ErrUnexpectedEOF, net.Error.Timeout() のパターンを見ることができます。

$ task
task: [build] go build -o app .

task: [run] ./app -timeout 100ms
07:54:29.313797 [C] recv start
07:54:29.414096 netErr.Timeout()

task: [run] ./app -timeout 3s
07:54:29.422673 [C] recv start
07:54:29.923413 io.ErrUnexpectedEOF

task: [run] ./app -length 10 -timeout 3s
07:54:29.930827 [C] recv start
07:54:29.931037 [C] data=(hhhh)
07:54:29.931064 [C] data=(hhhh)
07:54:30.431632 io.ErrUnexpectedEOF

task: [run] ./app -bufsize 6 -length 12 -timeout 3s
07:54:30.439863 [C] recv start
07:54:30.439944 [C] data=(hhhhhh)
07:54:30.439959 [C] data=(hhhhhh)
07:54:30.940426 io.EOF

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。




以上の内容はhttps://devlights.hatenablog.com/entry/2025/04/18/073000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14