1 Podstawy struktury

W języku Go, struktura jest typem złożonym służącym do agregacji różnych lub identycznych typów danych w pojedynczy byt. Struktury zajmują ważne miejsce w Go, ponieważ stanowią fundamentalny aspekt programowania obiektowego, chociaż z niewielkimi różnicami w porównaniu do tradycyjnych języków programowania obiektowego.

Potrzeba struktur wynika z następujących aspektów:

  • Organizacja zmiennych o silnym powiązaniu w celu zwiększenia czytelności kodu.
  • Zapewnienie możliwości symulowania "klas", ułatwiając enkapsulację i zagregację.
  • W interakcji z strukturami danych, takimi jak JSON, rekordami bazy danych, itp., struktury oferują wygodne narzędzie mapowania.

Organizacja danych za pomocą struktur pozwala na klarowne odwzorowanie modeli obiektów rzeczywistego świata, takich jak użytkownicy, zamówienia, itp.

2 Definiowanie struktury

Składnia definiowania struktury wygląda następująco:

type NazwaStruktury struct {
    Pole1 TypPola1
    Pole2 TypPola2
    // ... inne zmienne członkowskie
}
  • Kluczowe słowo type wprowadza definicję struktury.
  • NazwaStruktury to nazwa typu struktury, zgodnie z konwencjami nazewnictwa Go, zazwyczaj zapisanej wielką literą, aby wskazać jej eksportowalność.
  • Kluczowe słowo struct oznacza, że jest to typ struktury.
  • Wewnątrz nawiasów {}, definiuje się zmienne członkowskie struktury, z ich typami.

Typ zmiennej członkowskiej struktury może być dowolny, włącznie z typami podstawowymi (takimi jak int, string, itp.) oraz typami złożonymi (takimi jak tablice, slice, inna struktura, itp.).

Na przykład, definiowanie struktury reprezentującej osobę:

type Osoba struct {
    Imię   string
    Wiek    int
    Emaile []string // może zawierać typy złożone, takie jak slice
}

W powyższym kodzie, struktura Osoba ma trzy zmienne członkowskie: Imię typu string, Wiek typu całkowitego oraz Emaile typu slice string, co wskazuje, że osoba może mieć wiele adresów e-mail.

3 Tworzenie i Inicjowanie struktury

3.1 Tworzenie egzemplarza struktury

Istnieją dwa sposoby tworzenia egzemplarza struktury: bezpośrednie deklarowanie lub z użyciem słowa kluczowego new.

Bezpośrednie deklarowanie:

var o Osoba

Powyższy kod tworzy egzemplarz o typu Osoba, gdzie każda zmienna członkowska struktury ma wartość zerową odpowiadającą swojemu typowi.

Z użyciem słowa kluczowego new:

o := new(Osoba)

Tworzenie struktury za pomocą słowa kluczowego new skutkuje utworzeniem wskaźnika do struktury. Zmienna o w tym momencie ma typ *Osoba, wskazując na nowo zaalokowaną zmienną typu Osoba, gdzie zmienne członkowskie zostały zainicjalizowane wartościami zerowymi.

3.2 Inicjowanie egzemplarzy struktury

Egzemplarze struktury można zainicjować jednocześnie podczas ich tworzenia, przy użyciu dwóch metod: ze wskazaniem pól lub bez wskazania pól.

Inicjowanie ze wskazaniem pól:

o := Osoba{
    Imię:   "Alicja",
    Wiek:    30,
    Emaile: []string{"alicja@przykład.com", "alicja123@przykład.com"},
}

Podczas inicjowania za pomocą formy przypisania pól, kolejność inicjowania nie musi być taka sama jak kolejność deklaracji struktury, a niezainicjalizowane pola zachowają swoje wartości zerowe.

Inicjowanie bez wskazania pól:

o := Osoba{"Bartek", 25, []string{"bartek@przykład.com"}}

Podczas inicjowania bez wskazania pól, upewnij się, że początkowe wartości każdej zmiennej członkowskiej znajdują się w tej samej kolejności, co podczas definiowania struktury, i nie można pominąć pól.

Dodatkowo, struktury można zainicjować z określonymi polami, a wszelkie niesprecyzowane pola przyjmą wartości zerowe:

o := Osoba{Imię: "Krzysztof"}

W tym przykładzie, zainicjowano tylko pole Imię, podczas gdy Wiek i Emaile przyjmą swoje odpowiednie wartości zerowe.

4 Dostęp do zmiennych członkowskich struktury

Dostęp do zmiennych członkowskich struktury w Go jest bardzo prosty, osiąga się to za pomocą operatora kropki (.). Jeśli masz zmienną struktury, możesz odczytywać lub modyfikować wartości jej zmiennych członkowskich w ten sposób.

Przykład:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // Tworzymy zmienną typu Person
    p := Person{"Alice", 30}

    // Dostęp do pól struktury
    fmt.Println("Imię:", p.Name)
    fmt.Println("Wiek:", p.Age)

    // Modyfikacja wartości pól
    p.Name = "Bob"
    p.Age = 25

    // Ponowny dostęp do zmodyfikowanych wartości pól
    fmt.Println("\nZaktualizowane imię:", p.Name)
    fmt.Println("Zaktualizowany wiek:", p.Age)
}

W tym przykładzie najpierw definiujemy strukturę Person z dwoma polami: Name i Age. Następnie tworzymy instancję tej struktury i pokazujemy, jak odczytywać i modyfikować te pola.

5 Kompozycja i osadzanie struktur

Struktury mogą istnieć nie tylko niezależnie, ale także mogą być złożone i zagnieżdżone, aby tworzyć bardziej złożone struktury danych.

5.1 Anonimowe struktury

Anonimowa struktura nie deklaruje nowego typu, ale bezpośrednio używa definicji struktury. Jest to przydatne, gdy potrzebujesz utworzyć strukturę tylko raz i używać jej prosto, unikając tworzenia niepotrzebnych typów.

Przykład:

package main

import "fmt"

func main() {
    // Definiujemy i inicjalizujemy anonimową strukturę
    person := struct {
        Name string
        Age  int
    }{
        Name: "Ewa",
        Age:  40,
    }

    // Dostęp do pól anonimowej struktury
    fmt.Println("Imię:", person.Name)
    fmt.Println("Wiek:", person.Age)
}

W tym przykładzie, zamiast tworzyć nowy typ, bezpośrednio definiujemy strukturę i tworzymy jej instancję. Ten przykład pokazuje, jak zainicjalizować anonimową strukturę i odczytać jej pola.

5.2 Osadzanie struktur

Osadzanie struktur polega na zagnieżdżaniu jednej struktury jako pola innej struktury. Pozwala to tworzyć bardziej złożone modele danych.

Przykład:

package main

import "fmt"

// Definiowanie struktury Address
type Address struct {
    City    string
    Country string
}

// Osadź strukturę Address w strukturze Person
type Person struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    // Inicjalizacja instancji Person
    p := Person{
        Name: "Kacper",
        Age:  28,
        Address: Address{
            City:    "Nowy Jork",
            Country: "USA",
        },
    }

    // Dostęp do pól osadzonej struktury
    fmt.Println("Imię:", p.Name)
    fmt.Println("Wiek:", p.Age)
    // Dostęp do pól struktury Address
    fmt.Println("Miasto:", p.Address.City)
    fmt.Println("Kraj:", p.Address.Country)
}

W tym przykładzie definiujemy strukturę Address i osadzamy ją jako pole w strukturze Person. Tworząc instancję Person, tworzymy jednocześnie instancję Address. Możemy odczytywać pola osadzonej struktury za pomocą notacji kropkowej.

6 Metody struktur

Funkcje obiektowe (OOP) mogą być implementowane za pomocą metod struktur.

6.1 Podstawowe pojęcia metod

W języku Go, chociaż nie ma tradycyjnego pojęcia klas i obiektów, podobne cechy OOP można osiągnąć poprzez powiązanie metod z strukturami. Metoda struktury to specjalny rodzaj funkcji powiązanej z określonym typem struktury (lub wskaźnikiem do struktury), umożliwiając temu typowi posiadanie własnego zestawu metod.

// Definiowanie prostej struktury
type Rectangle struct {
    length, width float64
}

// Definiowanie metody dla struktury Rectangle do obliczania pola prostokąta
func (r Rectangle) Area() float64 {
    return r.length * r.width
}

W powyższym kodzie metoda Area jest powiązana z strukturą Rectangle. W definicji metody, (r Rectangle) to receiver, który określa, że ta metoda jest związana z typem Rectangle. Receiver pojawia się przed nazwą metody.

6.2 Odbiorniki wartości i odbiorniki wskaźników

Metody można podzielić na odbiorniki wartości i odbiorniki wskaźników w zależności od rodzaju odbiornika. Odbiorniki wartości używają kopii struktury do wywołania metody, podczas gdy odbiorniki wskaźników używają wskaźnika do struktury i mogą modyfikować oryginalną strukturę.

// Zdefiniuj metodę z odbiornikiem wartości
func (r Rectangle) Obwód() float64 {
    return 2 * (r.długość + r.szerokość)
}

// Zdefiniuj metodę z odbiornikiem wskaźników, która może modyfikować strukturę
func (r *Rectangle) UstawDługość(nowadługość float64) {
    r.długość = nowadługość // można zmodyfikować oryginalną wartość struktury
}

W powyższym przykładzie metoda Obwód jest metodą odbiornika wartości, wywołanie jej nie zmieni wartości Rectangle. Natomiast UstawDługość jest metodą odbiornika wskaźników, a wywołanie tej metody wpłynie na oryginalną instancję Rectangle.

6.3 Wywołanie Metody

Możesz wywołać metody struktury za pomocą zmiennej tej struktury oraz jej wskaźnika.

func main() {
    prostokąt := Rectangle{długość: 10, szerokość: 5}

    // Wywołaj metodę z odbiornikiem wartości
    fmt.Println("Powierzchnia:", prostokąt.Powierzchnia())

    // Wywołaj metodę z odbiornikiem wartości
    fmt.Println("Obwód:", prostokąt.Obwód())

    // Wywołaj metodę z odbiornikiem wskaźników
    prostokąt.UstawDługość(20)

    // Ponownie wywołaj metodę z odbiornikiem wartości, zauważ, że długość została zmodyfikowana
    fmt.Println("Po modyfikacji, Powierzchnia:", prostokąt.Powierzchnia())
}

Podczas wywoływania metody za pomocą wskaźnika, Go automatycznie obsługuje konwersję między wartościami i wskaźnikami, niezależnie od tego, czy metoda jest zdefiniowana z odbiornikiem wartości czy wskaźnikiem.

6.4 Wybór Typu Odbiornika

Przy definiowaniu metod warto zdecydować, czy użyć odbiornika wartości czy wskaźnika w zależności od sytuacji. Oto kilka ogólnych wytycznych:

  • Jeśli metoda ma modyfikować zawartość struktury, użyj odbiornika wskaźnika.
  • Jeśli struktura jest duża, a koszt kopiowania jest wysoki, użyj odbiornika wskaźnika.
  • Jeśli chcesz, aby metoda modyfikowała wartość wskazywaną przez odbiornik, użyj odbiornika wskaźnika.
  • Z przyczyn wydajnościowych, nawet jeśli nie modyfikujesz zawartości struktury, uzasadnione jest użycie odbiornika wskaźnika dla dużej struktury.
  • Dla małych struktur, lub gdy czytasz dane bez konieczności ich modyfikacji, odbiornik wartości jest zwykle prostszy i wydajniejszy.

Przy użyciu metod struktury możemy symulować niektóre cechy programowania zorientowanego obiektowo w Go, takie jak hermetyzacja i metody. Ten podejście w Go upraszcza koncepcję obiektów, jednocześnie zapewniając wystarczające możliwości do organizowania i zarządzania powiązanymi funkcjami.

7 Struktury i Serializacja JSON

W Go często konieczne jest serializowanie struktury do formatu JSON do transmisji sieciowej lub jako plik konfiguracyjny. Podobnie musimy być w stanie deserializować dane JSON do instancji struktury. Pakiet encoding/json w Go zapewnia tę funkcjonalność.

Oto przykład konwersji między strukturą a JSON-em:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Zdefiniuj strukturę Person i użyj tagów json do zdefiniowania mapowania między polami struktury a nazwami pól JSON
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Utwórz nową instancję Person
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// Serializuj do JSON-a
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("Serializacja JSON nie powiodła się: %s", err)
	}
	fmt.Printf("Format JSON: %s\n", jsonData)

	// Zdeserializuj do struktury
	var p2 Person
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("Deserializacja JSON nie powiodła się: %s", err)
	}
	fmt.Printf("Odzyskana Struktura: %#v\n", p2)
}

W powyższym kodzie zdefiniowaliśmy strukturę Person, zawierającą pole typu slice z opcją "omitempty". Ta opcja określa, że jeśli pole jest puste lub brakujące, nie zostanie uwzględnione w JSON-ie.

Użyliśmy funkcji json.Marshal do serializacji instancji struktury do JSON-a oraz funkcji json.Unmarshal do deserializacji danych JSON do instancji struktury.

8 Zaawansowane Tematy w Strukturach

8.1 Porównywanie Struktur

W języku Go, dozwolone jest bezpośrednie porównywanie dwóch instancji struktur, jednak porównanie to oparte jest na wartościach pól wewnątrz struktur. Jeśli wszystkie wartości pól są równe, to dwie instancje struktur są uważane za równe. Należy zauważyć, że nie wszystkie typy pól można porównywać. Na przykład struktura zawierająca slice'ów nie może być bezpośrednio porównywana.

Poniżej znajduje się przykład porównania struktur:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Wynik: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Wynik: p1 == p3: false
}

W tym przykładzie p1 i p2 są uważane za równe, ponieważ wszystkie wartości ich pól są takie same. Natomiast p3 nie jest równy p1, ponieważ wartość Y jest inna.

8.2 Kopiowanie Struktur

W języku Go, instancje struktur mogą być kopiowane poprzez przypisanie. Czy kopia ta jest głęboką kopią czy płytką kopią zależy od typów pól wewnątrz struktury.

Jeśli struktura zawiera tylko podstawowe typy (takie jak int, string, itp.), kopia jest głęboką kopią. Jeśli struktura zawiera typy referencyjne (takie jak slice'y, mapy, itp.), kopia będzie płytką kopią, a oryginalna instancja i nowo skopiowana instancja będą współdzielić pamięć typów referencyjnych.

Poniżej znajduje się przykład kopiowania struktury:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// Zainicjuj instancję struktury Data
original := Data{Numbers: []int{1, 2, 3}}

// Skopiuj strukturę
copied := original

// Zmodyfikuj elementy skopiowanego slice'a
copied.Numbers[0] = 100

// Wyświetl elementy oryginalnej i skopiowanej instancji
fmt.Println("Oryginał:", original.Numbers) // Wynik: Oryginał: [100 2 3]
fmt.Println("Skopiowany:", copied.Numbers) // Wynik: Skopiowany: [100 2 3]
}

Jak pokazano w przykładzie, instancje original i copied współdzielą ten sam slice, więc modyfikowanie danych slice'a w copied wpłynie również na dane slice'a w original.

Aby uniknąć tego problemu, można osiągnąć prawdziwe głębokie kopiowanie poprzez jawnie skopiowanie zawartości slice'a do nowego slice'a:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

W ten sposób, wszelkie modyfikacje w copied nie będą miały wpływu na original.