Przegląd generowania kodu
Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 84 z 107.
Generowanie kodu jest alternatywą dla refleksji, która tworzy kod źródłowy Go podczas kompilacji zamiast sprawdzać typy w czasie wykonywania. To podejście zapewnia bezpieczeństwo typów i lepszą wydajność, a jednocześnie ogranicza powtarzalny kod szablonowy.
Go udostępnia polecenie go generate, które automatyzuje generowanie kodu. Umieszczasz w pliku źródłowym specjalną dyrektywę w komentarzu, a uruchomienie go generate wykonuje wskazane narzędzie:
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Completed
)Uruchomienie go generate ./... wywołuje narzędzie stringer, które tworzy nowy plik zawierający metodę String() dla typu Status. Wygenerowany kod to zwykły kod Go, który jest kompilowany wraz z programem.
Popularne narzędzia do generowania kodu w ekosystemie Go to między innymi stringer do tworzenia metod zwracających ciągi znaków dla typów wyliczeniowych, mockgen do tworzenia atrap testowych na podstawie interfejsów oraz protoc do generowania kodu z definicji Protocol Buffer. Wiele ORM-ów i frameworków API również korzysta z generowania kodu, aby tworzyć bezpieczne pod względem typów zapytania do baz danych lub kod klienta.
Kluczową zaletą w porównaniu z refleksją jest to, że wygenerowany kod jest sprawdzany przez kompilator. Błędy są wykrywane podczas kompilacji, a nie w czasie działania programu. Ceną za to jest dodatkowy etap kompilacji i pliki generowane, którymi trzeba zarządzać w repozytorium. W przypadku wzorców OOP wymagających powtarzalnych implementacji dla wielu typów generowanie kodu często zapewnia najlepszy kompromis między bezpieczeństwem a wygodą.
Wyzwanie
ŁatwyZbudujmy generator kodu, który tworzy metody String() dla typów przypominających wyliczenia! Chociaż w tym środowisku nie możemy uruchomić rzeczywistego go generate, zasymulujesz działanie narzędzi do generowania kodu: programowe tworzenie kodu źródłowego Go na podstawie definicji typów.
Rozmieścisz kod w dwóch plikach:
generator.go: Utwórz logikę generowania kodu.Zbuduj strukturę
EnumType, która reprezentuje definicję typu przypominającego wyliczenie, z polemName(string) i polemValues(slice ciągów znaków reprezentujących nazwy stałych).Utwórz funkcję
GenerateStringMethod(e EnumType) string, która generuje kod źródłowy Go dla metodyString(). Wygenerowany kod powinien mieć następującą postać:func (e TypeName) String() string { switch e { case Value1: return "Value1" case Value2: return "Value2" default: return "Unknown" } }Utwórz również funkcję
GenerateConstants(e EnumType) string, która generuje deklaracje stałych przy użyciuiota:const ( Value1 TypeName = iota Value2 Value3 )Na koniec utwórz
GenerateFullCode(e EnumType) string, która łączy oba wyniki i dodaje komentarz nagłówkowy informujący, że jest to wygenerowany kod:// Code generated by enum generator. DO NOT EDIT. [constants] [string method]main.go: Użyj generatora, aby wygenerować kod dla różnych typów przypominających wyliczenia.Wczytaj nazwę typu, a następnie liczbę wartości i kolejne nazwy wartości. Przekaż te informacje do generatora i wypisz kompletny wygenerowany kod.
Zostaną podane następujące dane wejściowe:
- Wiersz 1: Nazwa typu przypominającego wyliczenie
- Wiersz 2: Liczba wartości
- Kolejne wiersze: Nazwa każdej wartości
Na przykład dla danych:
Status
3
Pending
Active
CompletedWynik powinien być następujący:
// Code generated by enum generator. DO NOT EDIT.
const (
Pending Status = iota
Active
Completed
)
func (e Status) String() string {
switch e {
case Pending:
return "Pending"
case Active:
return "Active"
case Completed:
return "Completed"
default:
return "Unknown"
}
}A dla danych:
Priority
2
Low
HighWynik powinien być następujący:
// Code generated by enum generator. DO NOT EDIT.
const (
Low Priority = iota
High
)
func (e Priority) String() string {
switch e {
case Low:
return "Low"
case High:
return "High"
default:
return "Unknown"
}
}To ćwiczenie pokazuje podstawową koncepcję stojącą za narzędziami takimi jak stringer: generowanie powtarzalnego, ale bezpiecznego typowo kodu podczas kompilacji zamiast używania refleksji w czasie wykonywania. Twój generator tworzy poprawny kod Go, który można zapisać do pliku i skompilować wraz z resztą projektu.
Spróbuj swoich sił
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Wczytaj nazwę typu wyliczeniowego
typeName, _ := reader.ReadString('\n')
typeName = strings.TrimSpace(typeName)
// Wczytaj liczbę wartości
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Wczytaj nazwę każdej wartości
values := make([]string, count)
for i := 0; i < count; i++ {
value, _ := reader.ReadString('\n')
values[i] = strings.TrimSpace(value)
}
// TODO: Utwórz EnumType na podstawie wczytanych danych
// TODO: Wygeneruj i wypisz pełny kod za pomocą GenerateFullCode
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy OOP w Go
Pliki zewnętrzneObszar roboczy i moduły GoPakiety i importyNazwy eksportowane i nieeksportowaneWprowadzenie do OOP w GoStruktury jako klasyDefiniowanie metod dla strukturOdbiorniki wskaźnikowe i wartościoweInicjalizacja strukturFunkcje konstruktoraPodsumowanie – prosty kalkulator4Interfejsy
Wprowadzenie do interfejsówImplementacja niejawnaInterfejs jako kontraktPusty interfejs (any)Asercja typuPrzełącznik typówKompozycja interfejsówInterfejsy Stringer i ErrorPowtórka — kalkulator figur7Hermetyzacja
Pola eksportowane i nieeksportowaneHermetyzacja na poziomie pakietuMetody pobierające i ustawiająceUkrywanie informacji w GoPowtórka – dane studentów10Typy generyczne (Go 1.18+)
Wprowadzenie do typów generycznychParametry typówOgraniczenia typówStruktury generyczneObejście ograniczeń metod generycznychPodsumowanie — generyczna kolekcja2Typy i struktury w praktyce
Typy proste i złożoneDefiniowanie własnych typówTagi pól strukturStruktury anonimoweStruktury zagnieżdżoneWartości zerowe i domyślnePodsumowanie – książka kontaktów5Kompozycja zamiast dziedziczenia
Dlaczego Go nie ma dziedziczeniaPodstawy osadzania strukturPromowanie metodOsadzanie wielu strukturOsadzanie a agregacjaPrzesłanianie osadzonych metodPodsumowanie — hierarchia pracowników8Obsługa błędów i programowanie obiektowe
Interfejs błęduWłasne typy błędówOpakowywanie błędów (fmt.Errorf)Błędy wartowniczeerrors.Is() i errors.As()Panic, Defer i RecoverPodsumowanie — parser plików3Wskaźniki i pamięć
Podstawy wskaźników w GoWskaźniki do strukturPrzekazywanie przez wartość a przez referencjęFunkcja new()Odśmiecanie pamięci w GoPowtórka — kreator listy wiązanej6Polimorfizm w Go
Polimorfizm za pomocą interfejsówDuck typing w GoZasady spełniania interfejsówKolekcje polimorficzneWstrzykiwanie zależnościPowtórka — procesor płatności9Współbieżność i OOP
Podstawy goroutinesKanały i komunikacjaKanały buforowane i niebuforowaneInstrukcja selectsync.Mutex i sync.RWMutexsync.WaitGroupProjektowanie struktur bezpiecznych wątkowoPowtórka – pula workerów12Zaawansowane zagadnienia programowania obiektowego
Wzorzec opcji funkcyjnychWzorzec Builder w GoŁańcuchowe wywoływanie metodAliasy typów a definicje typówPodstawy refleksjiPrzegląd generowania koduPoćwicz samodzielnie: Kompilator Go online