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.
package mainimport ("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]anyscanner := bufio.NewScanner(file)// Zwieksz bufor dla linii dluzszych niz 64KBscanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)for scanner.Scan() {var record map[string]anyif 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.
package mainimport ("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 []Userscanner := bufio.NewScanner(file)lineNum := 0for scanner.Scan() {lineNum++var u Userif 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.
package mainimport ("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 intfor {var entry LogEntryerr := 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 dekodowaniaif 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.
package mainimport ("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.
package mainimport ("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 wyjsciaencoder.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 plikuif 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.
package mainimport ("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 procesorscore := len(r.Data) * r.IDreturn 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 workeryvar wg sync.WaitGroupfor 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 skonczago func() {wg.Wait()close(results)}()// Czytaj plik i wysylaj rekordy do workerowgo func() {scanner := bufio.NewScanner(file)for scanner.Scan() {var r Recordif err := json.Unmarshal(scanner.Bytes(), &r); err != nil {log.Printf("pomijanie nieprawidlowej linii: %v", err)continue}jobs <- r}close(jobs)}()// Zbierz i zapisz wynikioutFile, err := os.Create("results.jsonl")if err != nil {log.Fatal(err)}defer outFile.Close()writer := bufio.NewWriter(outFile)encoder := json.NewEncoder(writer)var count intfor 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.
package mainimport ("bufio""encoding/json""fmt""log""os")type ParseError struct {Line intErr errorRaw string}func (e *ParseError) Error() string {return fmt.Sprintf("linia %d: %v", e.Line, e.Err)}type JSONLReader struct {scanner *bufio.ScannerlineNum intErrors []ParseErrorSkipped intSuccess 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 linieif len(line) == 0 {continue}var record map[string]anyif 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]anyif 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
ElastycznyStandardowy 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
ZalecanyStrumieniowy 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
ZapisStrumieniowy 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.