JSONL w Go: bufio, json.Decoder i wspolbieznosc

Kompletny przewodnik pracy z plikami JSONL (JSON Lines) w Go. Naucz sie czytac, zapisywac, strumieniowac i wspolbieznie przetwarzac dane JSONL przy uzyciu standardowej biblioteki bufio, encoding/json i goroutine.

Ostatnia aktualizacja: luty 2026

Dlaczego Go do JSONL?

Go to doskonaly wybor do przetwarzania plikow JSONL, szczegolnie gdy liczy sie wydajnosc i wspolbieznosc. Biblioteka standardowa zapewnia wszystko, czego potrzebujesz: bufio do wydajnego wejscia/wyjscia linia po linii, encoding/json do parsowania i serializacji oraz goroutine do rownoleglego przetwarzania. Nie sa wymagane zadne zewnetrzne zaleznosci. Kompilowany charakter Go oznacza, ze Twoj potok JSONL bedzie dzialal znacznie szybciej niz rownowazne skrypty Python lub Node.js, czesto przetwarzajac miliony linii na sekunde na skromnym sprzecie.

JSONL (JSON Lines) przechowuje jeden obiekt JSON na linie, co stanowi naturalny dopasowanie do modelu strumieniowego wejscia/wyjscia Go. Zamiast ladowac caly plik do pamieci, mozesz czytac i przetwarzac rekordy jeden po drugim za pomoca skanera lub dekodera. W polaczeniu z lekkimi goroutine i kanalami Go mozesz budowac wspolbiezne potoki JSONL, ktore wykorzystuja wszystkie dostepne rdzenie procesora. W tym przewodniku nauczysz sie, jak czytac JSONL za pomoca bufio.Scanner, strumieniowac za pomoca json.Decoder, wydajnie zapisywac JSONL, wspolbieznie przetwarzac rekordy i solidnie obslugiwac bledy.

Czytanie plikow JSONL za pomoca bufio.Scanner

Najprostszy sposob czytania JSONL w Go to uzycie bufio.Scanner. Czyta plik linia po linii, a kazda linie parsujesz za pomoca json.Unmarshal. To podejscie daje pelna kontrole nad rozmiarem bufora i obsluga bledow.

Otworz plik, utworz skaner i iteruj linia po linii. Kazda linia jest parsowana do mapy lub struktury za pomoca json.Unmarshal. Dzieki temu zuzycie pamieci jest proporcjonalne do pojedynczego rekordu, a nie calego pliku.

Podstawowe czytanie za pomoca bufio.Scanner
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
func main() {
file, err := os.Open("data.jsonl")
if err != nil {
log.Fatal(err)
}
defer file.Close()
var records []map[string]any
scanner := bufio.NewScanner(file)
// Zwieksz bufor dla linii dluzszych niz 64KB
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
for scanner.Scan() {
var record map[string]any
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
log.Printf("pomijanie nieprawidlowego JSON: %v", err)
continue
}
records = append(records, record)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("Zaladowano %d rekordow\n", len(records))
}

W kodzie produkcyjnym parsuj rekordy JSONL do typowanych struktur Go zamiast generycznych map. Zapewnia to bezpieczenstwo typow na etapie kompilacji, lepsza wydajnosc i czytelniejszy kod. Zdefiniuj swoja strukture z tagami json, aby kontrolowac mapowanie pol.

Parsowanie do typowanych struktur
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
}
func readUsers(path string) ([]User, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("otwarcie pliku: %w", err)
}
defer file.Close()
var users []User
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
var u User
if err := json.Unmarshal(scanner.Bytes(), &u); err != nil {
return nil, fmt.Errorf("linia %d: %w", lineNum, err)
}
users = append(users, u)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("blad skanera: %w", err)
}
return users, nil
}
func main() {
users, err := readUsers("users.jsonl")
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%s (%s)\n", u.Name, u.Email)
}
}

Strumieniowanie za pomoca json.Decoder

Dla wysokowydajnego strumieniowania json.Decoder czyta bezposrednio z io.Reader bez posredniego dzielenia na linie. Obsluguje buforowanie wewnetrznie i jest zalecanym podejsciem dla duzych plikow JSONL lub strumieni sieciowych, gdzie chcesz minimalizowac alokacje.

json.NewDecoder opakowuje dowolny io.Reader i dekoduje wartosci JSON sekwencyjnie. Kazde wywolanie Decode czyta dokladnie jeden obiekt JSON. Gdy strumien sie konczy, zwraca io.EOF. Jest to wydajniejsze niz bufio.Scanner + json.Unmarshal, poniewaz unika kopiowania kazdej linii do osobnego bufora.

Strumieniowanie za pomoca json.Decoder
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
)
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Service string `json:"service"`
}
func processJSONLStream(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("otwarcie: %w", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
var count int
for {
var entry LogEntry
err := decoder.Decode(&entry)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("dekodowanie rekordu %d: %w", count+1, err)
}
count++
// Przetwarzaj kazdy rekord w momencie dekodowania
if entry.Level == "ERROR" {
fmt.Printf("[%s] %s: %s\n",
entry.Timestamp, entry.Service, entry.Message)
}
}
fmt.Printf("Przetworzono %d wpisow logu\n", count)
return nil
}
func main() {
if err := processJSONLStream("logs.jsonl"); err != nil {
log.Fatal(err)
}
}

Zapisywanie plikow JSONL w Go

Zapisywanie JSONL w Go jest proste: serializuj kazdy rekord za pomoca json.Marshal, dodaj bajt nowej linii i zapisz do pliku. Dla najlepszej wydajnosci opakuj writer pliku w bufio.Writer, aby zmniejszyc liczbe wywolan systemowych.

Uzyj json.Marshal do serializacji kazdego rekordu do bajtow JSON, a nastepnie zapisz je wraz ze znakiem nowej linii. To podejscie jest proste i dobrze sprawdza sie dla malych i srednich plikow.

Podstawowy zapis za pomoca json.Marshal
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func main() {
products := []Product{
{ID: 1, Name: "Laptop", Price: 999.99},
{ID: 2, Name: "Mouse", Price: 29.99},
{ID: 3, Name: "Keyboard", Price: 79.99},
}
file, err := os.Create("products.jsonl")
if err != nil {
log.Fatal(err)
}
defer file.Close()
for _, p := range products {
data, err := json.Marshal(p)
if err != nil {
log.Printf("pomijanie bledu marshal: %v", err)
continue
}
data = append(data, '\n')
if _, err := file.Write(data); err != nil {
log.Fatal(err)
}
}
fmt.Printf("Zapisano %d rekordow do products.jsonl\n", len(products))
}

Dla zapisu o duzej przepustowosci uzyj json.NewEncoder z bufio.Writer. Encoder automatycznie dodaje nowa linie po kazdym wywolaniu Encode, a buforowany writer laczy male zapisy w wieksze wywolania systemowe. Zawsze wywolaj Flush przed zamknieciem pliku.

Buforowany zapis za pomoca json.Encoder
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type Event struct {
Type string `json:"type"`
Timestamp int64 `json:"timestamp"`
Payload map[string]any `json:"payload"`
}
func writeEvents(path string, events []Event) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("tworzenie pliku: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
encoder := json.NewEncoder(writer)
// Wylacz escapowanie HTML dla czystszego wyjscia
encoder.SetEscapeHTML(false)
for i, event := range events {
if err := encoder.Encode(event); err != nil {
return fmt.Errorf("kodowanie zdarzenia %d: %w", i, err)
}
}
// Oproznij buforowane dane do pliku
if err := writer.Flush(); err != nil {
return fmt.Errorf("oproznij: %w", err)
}
fmt.Printf("Zapisano %d zdarzen do %s\n", len(events), path)
return nil
}
func main() {
events := []Event{
{Type: "click", Timestamp: 1700000001, Payload: map[string]any{"x": 120, "y": 450}},
{Type: "view", Timestamp: 1700000002, Payload: map[string]any{"page": "/home"}},
}
if err := writeEvents("events.jsonl", events); err != nil {
log.Fatal(err)
}
}

Wspolbiezne przetwarzanie JSONL za pomoca goroutine

Goroutine i kanaly Go umozliwiaja latwe budowanie wspolbieznych potokow JSONL. Wzorzec fan-out czyta rekordy z pliku, rozdziela je miedzy wiele goroutine roboczych i zbiera wyniki przez kanal. Jest to idealne rozwiazanie dla transformacji JSONL intensywnie obciazajacych procesor na duzych plikach.

Wspolbiezne przetwarzanie JSONL za pomoca goroutine
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"runtime"
"sync"
)
type Record struct {
ID int `json:"id"`
Data map[string]any `json:"data"`
}
type Result struct {
ID int `json:"id"`
Processed bool `json:"processed"`
Score int `json:"score"`
}
func process(r Record) Result {
// Symulacja pracy intensywnie obciazajacej procesor
score := len(r.Data) * r.ID
return Result{ID: r.ID, Processed: true, Score: score}
}
func main() {
file, err := os.Open("records.jsonl")
if err != nil {
log.Fatal(err)
}
defer file.Close()
numWorkers := runtime.NumCPU()
jobs := make(chan Record, numWorkers*2)
results := make(chan Result, numWorkers*2)
// Uruchom workery
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for record := range jobs {
results <- process(record)
}
}()
}
// Zamknij kanal wynikow gdy wszystkie workery skoncza
go func() {
wg.Wait()
close(results)
}()
// Czytaj plik i wysylaj rekordy do workerow
go func() {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var r Record
if err := json.Unmarshal(scanner.Bytes(), &r); err != nil {
log.Printf("pomijanie nieprawidlowej linii: %v", err)
continue
}
jobs <- r
}
close(jobs)
}()
// Zbierz i zapisz wyniki
outFile, err := os.Create("results.jsonl")
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
writer := bufio.NewWriter(outFile)
encoder := json.NewEncoder(writer)
var count int
for result := range results {
if err := encoder.Encode(result); err != nil {
log.Printf("blad kodowania: %v", err)
}
count++
}
writer.Flush()
fmt.Printf("Przetworzono %d rekordow za pomoca %d workerow\n", count, numWorkers)
}

Ten wzorzec fan-out rozdziela rekordy JSONL miedzy goroutine rowne liczbie rdzeni procesora. Buforowane kanaly zapobiegaja blokowaniu goroutine przy wysylaniu i odbieraniu. sync.WaitGroup zapewnia, ze wszystkie workery zakoncza prace przed zamknieciem kanalu wynikow. Dla zadan intensywnie obciazajacych I/O, takich jak wywolania API, mozesz zwiekszyc numWorkers powyzej liczby rdzeni. Pamietaj, ze przy wspolbieznym przetwarzaniu kolejnosc wyjscia nie jest gwarantowana.

Najlepsze praktyki obslugi bledow

Solidne przetwarzanie JSONL w Go wymaga starannej obslugi bledow. Rzeczywiste pliki JSONL moga zawierac znieksztalcone linie, problemy z kodowaniem lub nieoczekiwanie duze rekordy. Ten wzorzec zapewnia sledzenie bledow na poziomie linii, konfigurowalne pomijanie bledow i raportowanie podsumowania.

Najlepsze praktyki obslugi bledow
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type ParseError struct {
Line int
Err error
Raw string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("linia %d: %v", e.Line, e.Err)
}
type JSONLReader struct {
scanner *bufio.Scanner
lineNum int
Errors []ParseError
Skipped int
Success int
}
func NewJSONLReader(file *os.File) *JSONLReader {
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
return &JSONLReader{scanner: scanner}
}
func (r *JSONLReader) ReadAll(target any) error {
slice, ok := target.(*[]map[string]any)
if !ok {
return fmt.Errorf("target musi byc *[]map[string]any")
}
for r.scanner.Scan() {
r.lineNum++
line := r.scanner.Bytes()
// Pomin puste linie
if len(line) == 0 {
continue
}
var record map[string]any
if err := json.Unmarshal(line, &record); err != nil {
r.Errors = append(r.Errors, ParseError{
Line: r.lineNum,
Err: err,
Raw: string(line),
})
r.Skipped++
continue
}
*slice = append(*slice, record)
r.Success++
}
return r.scanner.Err()
}
func (r *JSONLReader) Summary() string {
return fmt.Sprintf(
"Linie: %d | Sukces: %d | Pominiete: %d | Bledy: %d",
r.lineNum, r.Success, r.Skipped, len(r.Errors),
)
}
func main() {
file, err := os.Open("data.jsonl")
if err != nil {
log.Fatal(err)
}
defer file.Close()
reader := NewJSONLReader(file)
var records []map[string]any
if err := reader.ReadAll(&records); err != nil {
log.Fatal(err)
}
fmt.Println(reader.Summary())
for _, e := range reader.Errors {
log.Printf("Blad parsowania w %s", e.Error())
}
}

Ta struktura JSONLReader sledzi numery linii, zbiera bledy parsowania z ich surowa trescia i raportuje podsumowanie sukcesow i niepowodzen. Bufor skanera o rozmiarze 10MB obsluguje nietypowo duze linie bez paniki. W produkcji mozesz rozszerzyc to o obsluge context.Context do anulowania, maksymalny prog bledow przerywajacy przetwarzanie lub strukturalne logowanie za pomoca slog.

Pakiety Go do przetwarzania JSONL

Biblioteka standardowa Go zapewnia wszystkie elementy skladowe potrzebne do przetwarzania JSONL. Oto trzy podstawowe podejscia i kiedy uzywac kazdego z nich.

bufio.Scanner

Elastyczny

Standardowy czytnik linia po linii. Laczy sie z json.Unmarshal do jawnego parsowania. Najlepszy, gdy potrzebujesz kontroli nad rozmiarem bufora, chcesz pomijac lub sprawdzac surowe linie lub potrzebujesz numerow linii do raportowania bledow. Obsluguje linie do skonfigurowanego rozmiaru bufora.

json.Decoder

Zalecany

Strumieniowy dekoder JSON, ktory czyta bezposrednio z io.Reader. Wydajniejszy niz Scanner + Unmarshal, poniewaz unika kopiowania danych. Idealny dla duzych plikow i strumieni sieciowych. Automatycznie obsluguje kolejne wartosci JSON bez potrzeby jawnego dzielenia po nowych liniach.

json.Encoder

Zapis

Strumieniowy koder JSON, ktory zapisuje do io.Writer. Automatycznie dodaje nowa linie po kazdym wywolaniu Encode, co czyni go idealnym do wyjscia JSONL. Polacz z bufio.Writer dla zapisu o duzej przepustowosci. Obsluguje konfiguracje SetEscapeHTML i SetIndent.

Wyprobuj nasze darmowe narzedzia JSONL

Nie chcesz pisac kodu? Uzyj naszych darmowych narzedzi online, aby przegladac, walidowac i konwertowac pliki JSONL bezposrednio w przegladarce.

Pracuj z plikami JSONL online

Przegladaj, waliduj i konwertuj pliki JSONL do 1GB bezposrednio w przegladarce. Bez przesylania, 100% prywatnosci.

Czesto zadawane pytania

JSONL w Go — bufio.Scanner, json.Decoder i współbieżność ...