Understanding Unexpected EOF Errors
EOF (End of File) errors in Go appear as:
unexpected EOF
EOF
read tcp: use of closed network connection
io: read/write on closed pipeEOF 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) } } ```