Understanding Unexpected EOF Errors

EOF (End of File) errors in Go appear as:

bash
unexpected EOF
EOF
read tcp: use of closed network connection
io: read/write on closed pipe

EOF can be a normal condition (reached end of file) or an error (unexpected termination).

Common Scenarios and Solutions

Scenario 1: Reading File with Incomplete Data

Problem code: ``go func main() { // Attempting to read exact number of bytes data := make([]byte, 1024) n, err := file.Read(data) if err != nil { log.Fatal(err) // unexpected EOF if file is smaller } }

Solution - Use io.ReadFull or handle partial reads: ```go func main() { data := make([]byte, 1024)

// Option 1: Use io.ReadFull (expects exactly 1024 bytes) n, err := io.ReadFull(file, data) if err == io.ErrUnexpectedEOF { // Got fewer bytes than expected fmt.Printf("Read only %d bytes instead of %d\n", n, len(data)) } else if err == io.EOF { // Read 0 bytes, file was empty or at end } else if err != nil { log.Fatal(err) }

// Option 2: Use io.ReadAll for variable length data, err := io.ReadAll(file) if err != nil { log.Fatal(err) }

// Option 3: Read in chunks buf := make([]byte, 1024) total := 0 for { n, err := file.Read(buf) total += n if err == io.EOF { break // Normal end of file } if err != nil { log.Fatal(err) // Other error } process(buf[:n]) } } ```

Scenario 2: HTTP Response Body Partial Read

Problem code: ```go func main() { resp, err := http.Get("https://api.example.com/data") if err != nil { log.Fatal(err) } defer resp.Body.Close()

// Server sends 500 bytes but we try to read 1000 data := make([]byte, 1000) _, err = io.ReadFull(resp.Body, data) if err != nil { log.Fatal(err) // unexpected EOF } } ```

Solution - Read what's available: ```go func main() { resp, err := http.Get("https://api.example.com/data") if err != nil { log.Fatal(err) } defer resp.Body.Close()

// Check Content-Length if available if resp.ContentLength > 0 && resp.ContentLength < 1000 { fmt.Printf("Server only sending %d bytes\n", resp.ContentLength) }

// Read all available data data, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) }

// Or use limited reader for safety maxBytes := int64(10 * 1024 * 1024) // 10 MB max limited := io.LimitReader(resp.Body, maxBytes) data, err = io.ReadAll(limited) } ```

Scenario 3: Network Connection Closed Prematurely

Problem code: ``go func handleConnection(conn net.Conn) { data := make([]byte, 4096) for { n, err := conn.Read(data) if err != nil { // Could be EOF (client closed normally) or unexpected EOF log.Println("Connection error:", err) return } process(data[:n]) } }

Solution - Handle different EOF scenarios: ```go func handleConnection(conn net.Conn) { defer conn.Close()

data := make([]byte, 4096) for { n, err := conn.Read(data) if n > 0 { process(data[:n]) }

if err != nil { if err == io.EOF { // Client closed connection cleanly log.Println("Client closed connection") return }

// Check for network-specific errors if netErr, ok := err.(net.Error); ok { if netErr.Timeout() { log.Println("Read timeout") return } }

// Unexpected error log.Println("Connection error:", err) return } } } ```

Scenario 4: Pipe Writer Closed Before Data Complete

Problem code: ```go func main() { r, w := io.Pipe()

go func() { w.Write([]byte("partial data")) w.CloseWithError(errors.New("interrupted")) }()

data, err := io.ReadAll(r) if err != nil { // Error: interrupted (wrapped EOF) log.Fatal(err) } } ```

Solution - Handle pipe errors gracefully: ```go func main() { r, w := io.Pipe()

go func() { defer w.Close()

for i := 0; i < 10; i++ { data := fmt.Sprintf("chunk %d\n", i) if _, err := w.Write([]byte(data)); err != nil { // Reader side closed return } } }()

// Read with error handling data, err := io.ReadAll(r) if err != nil { if err == io.EOF { // Normal completion } else { log.Println("Pipe error:", err) } } fmt.Println(string(data)) } ```

Scenario 5: Archive/Tar Reading

Problem code: ```go func main() { file, _ := os.Open("archive.tar") tarReader := tar.NewReader(file)

for { header, err := tarReader.Next() if err != nil { // Could be EOF or corrupted archive log.Fatal(err) } // Process file... } } ```

Solution - Differentiate EOF from errors: ```go func main() { file, err := os.Open("archive.tar") if err != nil { log.Fatal(err) } defer file.Close()

tarReader := tar.NewReader(file)

for { header, err := tarReader.Next() if err == io.EOF { // Normal end of archive break } if err != nil { // Corrupted or incomplete archive log.Fatal("Archive error:", err) }

// Read file content content, err := io.ReadAll(tarReader) if err != nil { log.Printf("Error reading %s: %v", header.Name, err) continue }

fmt.Printf("File: %s, Size: %d\n", header.Name, len(content)) } } ```

Scenario 6: JSON Decoder Unexpected EOF

Problem code: ``go func main() { // Malformed JSON: missing closing brace data := []byte({"name": "Alice"`)

var user struct { Name string json:"name" }

err := json.Unmarshal(data, &user) // Error: unexpected end of JSON input } ```

Solution - Validate JSON before parsing: ``go func main() { data := []byte({"name": "Alice"`)

// Check if JSON is valid if !json.Valid(data) { log.Fatal("Invalid JSON data") }

// Or use decoder for streaming with better errors decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields()

var user struct { Name string json:"name" }

err := decoder.Decode(&user) if err != nil { if err == io.EOF { log.Fatal("Empty JSON input") }

// Get detailed syntax error if syntaxErr, ok := err.(*json.SyntaxError); ok { log.Fatalf("JSON syntax error at offset %d: %v", syntaxErr.Offset, err) }

log.Fatal(err) } } ```

Robust I/O Reading Pattern

```go type SafeReader struct { reader io.Reader maxBytes int64 }

func NewSafeReader(r io.Reader, max int64) *SafeReader { return &SafeReader{ reader: io.LimitReader(r, max), maxBytes: max, } }

func (sr *SafeReader) ReadAll() ([]byte, error) { data, err := io.ReadAll(sr.reader) if err != nil { if err == io.EOF { // Check if we hit the limit if int64(len(data)) >= sr.maxBytes { return data, fmt.Errorf("data truncated at %d bytes", sr.maxBytes) } // Normal EOF before limit return data, nil } return nil, fmt.Errorf("read error: %w", err) } return data, nil } ```

Network Reading with Timeout

```go func readWithTimeout(conn net.Conn, timeout time.Duration) ([]byte, error) { buf := make([]byte, 4096)

if timeout > 0 { conn.SetReadDeadline(time.Now().Add(timeout)) }

total := 0 chunks := make([][]byte, 0)

for { n, err := conn.Read(buf) if n > 0 { chunk := make([]byte, n) copy(chunk, buf[:n]) chunks = append(chunks, chunk) total += n }

if err != nil { if err == io.EOF { // Connection closed by peer break } if netErr, ok := err.(net.Error); ok && netErr.Timeout() { // Read timeout - return what we have break } return nil, err } }

// Combine chunks result := make([]byte, total) offset := 0 for _, chunk := range chunks { copy(result[offset:], chunk) offset += len(chunk) }

return result, nil } ```

Distinguishing EOF Types

```go func classifyEOF(err error) string { if err == nil { return "no error" }

if err == io.EOF { return "clean EOF" }

if err == io.ErrUnexpectedEOF { return "unexpected EOF (partial data)" }

// Check if error wraps EOF if errors.Is(err, io.EOF) { return "wrapped EOF" }

return "other error" } ```

Verification

```go func TestEOFHandling(t *testing.T) { // Test clean EOF emptyFile := bytes.NewReader([]byte{}) data, err := io.ReadAll(emptyFile) if err != nil { t.Errorf("empty file should give clean EOF, not error: %v", err) } if len(data) != 0 { t.Error("empty file should give empty data") }

// Test unexpected EOF partialData := bytes.NewReader([]byte("abc")) buf := make([]byte, 10) n, err := io.ReadFull(partialData, buf) if err != io.ErrUnexpectedEOF { t.Errorf("expected ErrUnexpectedEOF, got: %v", err) } if n != 3 { t.Errorf("expected 3 bytes read, got: %d", n) } } ```