Go dla początkujących - Część 14: Obsługa plików

2/13/2025 Kurs Go

Mateusz Kędziora

image

Hej! Oto kolejny artykuł z serii “Go dla początkujących”! Dziś zajmiemy się obsługą plików, co jest niezwykle ważnym aspektem każdego języka programowania. Nauczymy się, jak odczytywać i zapisywać pliki, obsługiwać przesyłanie i pobieranie plików przez HTTP oraz jak serwować statyczne zasoby, takie jak CSS, JavaScript i obrazy.

Ten artykuł skupia się na praktycznym zastosowaniu wiedzy. Przygotujcie się na mnóstwo kodu i konkretnych przykładów!

Wprowadzenie do Obsługi Plików w Go

Go oferuje rozbudowany pakiet os do pracy z systemem operacyjnym, w tym z plikami. Poniżej przedstawiamy podstawowe operacje, które będziemy wykorzystywać:

  • Otwieranie pliku: Funkcja os.Open() otwiera plik do odczytu.
  • Tworzenie pliku: Funkcja os.Create() tworzy nowy plik.
  • Zamykanie pliku: Funkcja file.Close() zamyka otwarty plik.
  • Czytanie z pliku: Funkcja file.Read() czyta dane z pliku do bufora.
  • Zapisywanie do pliku: Funkcja file.Write() zapisuje dane z bufora do pliku.

Zanim zagłębimy się w obsługę przesyłania i pobierania plików, zobaczmy prosty przykład odczytu zawartości pliku.

Odczyt Pliku w Go

Oto przykład kodu, który odczytuje zawartość pliku o nazwie example.txt i wyświetla ją na ekranie:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
)

func main() {
	// Spróbuj odczytać cały plik do zmiennej 'content'
	content, err := ioutil.ReadFile("example.txt")

	// Sprawdź, czy wystąpił błąd podczas odczytu pliku
	if err != nil {
		log.Fatal(err) // Wyświetl błąd i zakończ program, jeśli wystąpił problem
	}

	// Wyświetl zawartość pliku jako string
	fmt.Println(string(content))
}

Wyjaśnienie kodu:

  1. Import pakietów: Importujemy pakiety fmt (do wyświetlania tekstu), io/ioutil (do prostego odczytu zawartości pliku) i log (do obsługi błędów).
  2. ioutil.ReadFile("example.txt"): Ta funkcja próbuje odczytać całą zawartość pliku example.txt i zwraca ją jako tablicę bajtów ([]byte). Zwraca również potencjalny błąd.
  3. if err != nil { ... }: Sprawdzamy, czy podczas odczytu pliku wystąpił błąd. Jeśli tak, to wypisujemy błąd za pomocą log.Fatal(err), co spowoduje również zakończenie działania programu.
  4. fmt.Println(string(content)): Konwertujemy tablicę bajtów (content) na string i wyświetlamy ją na ekranie.

Jak to uruchomić:

  1. Stwórz plik o nazwie example.txt w tym samym katalogu, w którym znajduje się Twój kod Go. Wpisz do niego dowolny tekst.
  2. Zapisz powyższy kod jako plik main.go.
  3. Uruchom program za pomocą komendy go run main.go.

Powinieneś zobaczyć zawartość pliku example.txt wypisaną w konsoli.

Zapis Pliku w Go

Teraz zobaczmy, jak zapisać dane do pliku. Użyjemy funkcji ioutil.WriteFile():

package main

import (
	"fmt"
	"io/ioutil"
	"log"
)

func main() {
	// Dane do zapisania w pliku
	data := []byte("To jest tekst, który zostanie zapisany w pliku.")

	// Nazwa pliku
	filename := "output.txt"

	// Zapis danych do pliku
	err := ioutil.WriteFile(filename, data, 0644) // 0644 to uprawnienia pliku (rw-r--r--)

	// Sprawdzenie, czy wystąpił błąd
	if err != nil {
		log.Fatal(err) // Jeśli wystąpił błąd, wypisz go i zakończ program
	}

	// Informacja o sukcesie
	fmt.Printf("Zapisano dane do pliku: %s\n", filename)
}

Wyjaśnienie kodu:

  1. data := []byte("..."): Definiujemy tablicę bajtów data, która zawiera tekst do zapisania w pliku. Konwersja string na []byte jest konieczna, ponieważ ioutil.WriteFile oczekuje danych w formie bajtów.
  2. filename := "output.txt": Określamy nazwę pliku, do którego chcemy zapisać dane.
  3. ioutil.WriteFile(filename, data, 0644): Ta funkcja zapisuje zawartość tablicy bajtów data do pliku o nazwie filename. Trzeci argument, 0644, określa uprawnienia pliku. 0644 oznacza:
    • 6 dla właściciela pliku (odczyt i zapis)
    • 4 dla grupy użytkowników (tylko odczyt)
    • 4 dla pozostałych użytkowników (tylko odczyt)
  4. if err != nil { ... }: Sprawdzamy, czy podczas zapisu wystąpił błąd. Jeśli tak, to wypisujemy błąd i kończymy program.
  5. fmt.Printf("..."): Wypisujemy informację o sukcesie zapisu danych do pliku.

Jak to uruchomić:

  1. Zapisz powyższy kod jako plik main.go.
  2. Uruchom program za pomocą komendy go run main.go.

Po uruchomieniu programu, w tym samym katalogu powinien pojawić się plik o nazwie output.txt z zapisaną zawartością.

Obsługa Przesyłania Plików (Upload)

Teraz przejdźmy do bardziej zaawansowanego tematu: obsługi przesyłania plików przez HTTP. Zbudujemy prosty serwer, który przyjmuje plik przesłany przez formularz HTML.

1. Kod HTML Formularza (index.html):

<!DOCTYPE html>
<html>
<head>
    <title>Przesyłanie Plików</title>
</head>
<body>
    <h1>Prześlij Plik</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit" value="Prześlij">
    </form>
</body>
</html>

Wyjaśnienie:

  • action="/upload": Określa adres URL, na który formularz ma wysłać dane.
  • method="post": Używa metody POST do przesyłania danych (metoda GET ma ograniczenia co do rozmiaru danych).
  • enctype="multipart/form-data": Kluczowe! Ten atrybut informuje przeglądarkę, że formularz zawiera pliki i że dane mają być zakodowane w specjalny sposób, który umożliwia ich przesłanie.
  • <input type="file" name="file">: Tworzy pole do wyboru pliku. Atrybut name="file" jest ważny, ponieważ nazwa ta będzie używana przez serwer do identyfikacji przesłanego pliku.
  • <input type="submit" value="Prześlij">: Przycisk do wysłania formularza.

2. Kod Serwera Go (main.go):

package main

import (
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"os"
)

// Funkcja obsługująca wyświetlanie formularza
func indexHandler(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("index.html")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	err = tmpl.Execute(w, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// Funkcja obsługująca przesyłanie pliku
func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// Ograniczenie rozmiaru przesyłanego pliku (np. 10MB)
	err := r.ParseMultipartForm(10 << 20)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Pobranie pliku z formularza
	file, handler, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "Błąd podczas pobierania pliku", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Wyświetlenie informacji o pliku
	fmt.Fprintf(w, "Nazwa pliku: %s\n", handler.Filename)
	fmt.Fprintf(w, "Rozmiar pliku: %d\n", handler.Size)
	fmt.Fprintf(w, "Typ MIME: %s\n", handler.Header.Get("Content-Type"))

	// Utworzenie pliku na serwerze
	dst, err := os.Create("./uploads/" + handler.Filename)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	// Kopiowanie zawartości przesłanego pliku do nowego pliku
	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Plik został zapisany na serwerze jako: %s\n", handler.Filename)
}

func main() {
	// Utworzenie katalogu 'uploads', jeśli nie istnieje
	if _, err := os.Stat("./uploads"); os.IsNotExist(err) {
		err := os.Mkdir("./uploads", 0755) // 0755 to uprawnienia (rwxr-xr-x)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Obsługa routingu
	http.HandleFunc("/", indexHandler)         // Obsługa strony głównej (formularz)
	http.HandleFunc("/upload", uploadHandler) // Obsługa przesyłania pliku

	// Uruchomienie serwera na porcie 8080
	fmt.Println("Serwer uruchomiony na porcie 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Wyjaśnienie kodu:

  1. import: Importujemy potrzebne pakiety: fmt, html/template, io, log, net/http i os.
  2. indexHandler: Ta funkcja obsługuje żądania do strony głównej (/). Odpowiada za wyświetlenie formularza HTML (plik index.html).
    • template.ParseFiles("index.html"): Parsuje plik HTML i tworzy szablon.
    • tmpl.Execute(w, nil): Wykonuje szablon, wysyłając wynikowy HTML do przeglądarki.
  3. uploadHandler: Ta funkcja obsługuje żądania POST na adres /upload. Odpowiada za odbiór i zapisywanie przesłanego pliku.
    • r.ParseMultipartForm(10 << 20): Parsuje formularz, obsługując przesyłanie plików. 10 << 20 oznacza 10 MB (ograniczenie rozmiaru przesyłanego pliku).
    • r.FormFile("file"): Pobiera informacje o przesłanym pliku. “file” to nazwa pola w formularzu HTML (<input type="file" name="file">).
    • file.Close(): Zamyka przesłany plik po użyciu. defer zapewnia, że plik zostanie zamknięty, nawet jeśli wystąpi błąd.
    • os.Create("./uploads/" + handler.Filename): Tworzy nowy plik na serwerze w katalogu uploads. Nazwa pliku jest pobierana z przesłanego formularza (handler.Filename).
    • dst.Close(): Zamyka nowo utworzony plik po użyciu. defer zapewnia, że plik zostanie zamknięty, nawet jeśli wystąpi błąd.
    • io.Copy(dst, file): Kopiuje zawartość przesłanego pliku (file) do nowego pliku (dst).
    • Funkcja wypisuje też informacje o pliku (nazwa, rozmiar, typ MIME) w odpowiedzi HTTP.
  4. main:
    • os.Mkdir("./uploads", 0755): Tworzy katalog uploads, jeśli jeszcze nie istnieje. Uprawnienia 0755 oznaczają, że właściciel ma prawo do odczytu, zapisu i wykonywania, a grupa i inni użytkownicy mają prawo do odczytu i wykonywania.
    • http.HandleFunc("/", indexHandler): Rejestruje funkcję indexHandler do obsługi żądań do strony głównej (/).
    • http.HandleFunc("/upload", uploadHandler): Rejestruje funkcję uploadHandler do obsługi żądań do adresu /upload.
    • http.ListenAndServe(":8080", nil): Uruchamia serwer HTTP na porcie 8080.

Jak to uruchomić:

  1. Stwórz plik index.html z powyższą zawartością.
  2. Zapisz kod serwera Go jako main.go.
  3. Uruchom serwer za pomocą komendy go run main.go.
  4. Otwórz przeglądarkę i przejdź na adres http://localhost:8080.
  5. Wybierz plik i prześlij go.

Przesłany plik powinien zostać zapisany w katalogu uploads w tym samym katalogu, w którym uruchomiłeś serwer. Dodatkowo, w przeglądarce powinieneś zobaczyć informacje o przesłanym pliku.

Obsługa Pobierania Plików (Download)

Teraz zajmijmy się pobieraniem plików z serwera.

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
)

// Funkcja obsługująca pobieranie pliku
func downloadHandler(w http.ResponseWriter, r *http.Request) {
	// Pobranie nazwy pliku z URL (np. /download/example.txt)
	filename := filepath.Base(r.URL.Path)

	// Ścieżka do pliku
	filepath := filepath.Join("./uploads", filename)

	// Sprawdzenie, czy plik istnieje
	if _, err := os.Stat(filepath); os.IsNotExist(err) {
		http.Error(w, "Plik nie istnieje", http.StatusNotFound)
		return
	}

	// Ustawienie nagłówków HTTP, aby przeglądarka wiedziała, że to plik do pobrania
	w.Header().Set("Content-Disposition", "attachment; filename="+filename)
	w.Header().Set("Content-Type", "application/octet-stream") // Typ MIME dla plików binarnych

	// Otwarcie pliku
	file, err := os.Open(filepath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer file.Close()

	// Kopiowanie zawartości pliku do odpowiedzi HTTP
	_, err = io.Copy(w, file)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func main() {
	// Obsługa routingu
	http.HandleFunc("/download/", downloadHandler) // Obsługa pobierania plików

	// Uruchomienie serwera na porcie 8080
	fmt.Println("Serwer uruchomiony na porcie 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Wyjaśnienie kodu:

  1. downloadHandler: Ta funkcja obsługuje żądania pobierania plików.
    • filepath.Base(r.URL.Path): Pobiera nazwę pliku z adresu URL. Na przykład, jeśli URL to /download/example.txt, to funkcja zwróci “example.txt”.
    • filepath.Join("./uploads", filename): Tworzy pełną ścieżkę do pliku w katalogu uploads.
    • os.Stat(filepath): Sprawdza, czy plik istnieje.
    • w.Header().Set("Content-Disposition", "attachment; filename="+filename): Ustawia nagłówek HTTP, który informuje przeglądarkę, że to plik do pobrania. attachment oznacza, że przeglądarka ma wyświetlić okno dialogowe zapisu pliku.
    • w.Header().Set("Content-Type", "application/octet-stream"): Ustawia typ MIME na application/octet-stream, co oznacza, że to plik binarny.
    • io.Copy(w, file): Kopiuje zawartość pliku do odpowiedzi HTTP, co powoduje pobranie pliku przez przeglądarkę.
  2. main:
    • http.HandleFunc("/download/", downloadHandler): Rejestruje funkcję downloadHandler do obsługi żądań do adresu /download/. Zauważ, że używamy "/download/" a nie "/download". http.HandleFunc z dopiskiem / sprawia, że funkcja zostanie wywołana dla wszystkich adresów zaczynających się od /download/, np. /download/plik.txt, /download/folder/inny_plik.jpg.

Jak to uruchomić:

  1. Zapisz kod serwera Go jako main.go.
  2. Uruchom serwer za pomocą komendy go run main.go.
  3. Umieść pliki, które chcesz udostępnić do pobrania, w katalogu uploads.
  4. Otwórz przeglądarkę i przejdź na adres http://localhost:8080/download/nazwa_pliku.txt (zastąp nazwa_pliku.txt nazwą pliku, który chcesz pobrać).

Przeglądarka powinna wyświetlić okno dialogowe zapisu pliku.

Serwowanie Statycznych Zasobów (CSS, JavaScript, Obrazy)

Bardzo często będziesz chciał serwować statyczne zasoby, takie jak pliki CSS, JavaScript i obrazy, z serwera Go. Go oferuje do tego prosty sposób.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// Obsługa statycznych zasobów z katalogu "static"
	fs := http.FileServer(http.Dir("static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	// Obsługa strony głównej (opcjonalnie)
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Witaj na stronie głównej!")
	})

	// Uruchomienie serwera na porcie 8080
	fmt.Println("Serwer uruchomiony na porcie 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Wyjaśnienie kodu:

  1. fs := http.FileServer(http.Dir("static")): Tworzy handler HTTP, który serwuje pliki z katalogu “static”. http.Dir("static") tworzy interfejs FileSystem reprezentujący katalog static.
  2. http.Handle("/static/", http.StripPrefix("/static/", fs)): Rejestruje handler fs do obsługi żądań do adresu /static/. http.StripPrefix("/static/", fs) usuwa przedrostek “/static/” z adresu URL przed przekazaniem go do fs. Oznacza to, że jeśli zażądasz pliku /static/style.css, to fs otrzyma żądanie dla pliku style.css w katalogu static.
  3. http.HandleFunc("/", ...): (Opcjonalnie) Obsługuje stronę główną.
  4. http.ListenAndServe(":8080", nil): Uruchamia serwer HTTP na porcie 8080.

Jak to uruchomić:

  1. Stwórz katalog o nazwie static w tym samym katalogu, w którym znajduje się Twój kod Go.
  2. Umieść w katalogu static pliki CSS, JavaScript, obrazy, itp.
  3. Zapisz kod serwera Go jako main.go.
  4. Uruchom serwer za pomocą komendy go run main.go.
  5. Otwórz przeglądarkę i przejdź na adres http://localhost:8080/static/nazwa_pliku.css (zastąp nazwa_pliku.css nazwą pliku CSS, który chcesz wyświetlić) lub http://localhost:8080/static/obraz.jpg.

Przeglądarka powinna wyświetlić zawartość pliku CSS lub obrazu.

Przykład użycia w HTML:

Jeśli masz plik HTML, możesz odwołać się do plików CSS i JavaScript w katalogu static w następujący sposób:

<!DOCTYPE html>
<html>
<head>
    <title>Statyczne Zasoby</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <h1>Witaj!</h1>
    <img src="/static/obraz.jpg" alt="Obraz">
    <script src="/static/script.js"></script>
</body>
</html>

Podsumowanie

W tym artykule omówiliśmy podstawowe operacje związane z obsługą plików w Go: odczyt, zapis, przesyłanie, pobieranie i serwowanie statycznych zasobów. Mamy nadzieję, że ten przewodnik okazał się pomocny w zrozumieniu tych ważnych koncepcji.

Praca Domowa

  1. Rozbuduj serwer przesyłania plików: Dodaj walidację, która sprawdza typ przesyłanego pliku (np. czy to obraz, a nie plik wykonywalny). Wyświetl odpowiedni komunikat błędu, jeśli typ pliku jest nieprawidłowy.
  2. Dodaj listę plików do pobrania: Po przesłaniu pliku, dodaj go do listy dostępnej na stronie HTML. Lista powinna zawierać linki do pobrania każdego pliku.

Dodatkowe Materiały

Zachęcam do dalszego eksperymentowania z kodem i zapoznania się z pozostałymi artykułami z naszej serii “Go dla początkujących”! Powodzenia!

Polecane artykuły