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
.