Panoramica sulla generazione del codice
Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 84 di 107.
La generazione del codice è un’alternativa alla reflection che crea codice sorgente Go al momento della compilazione invece di esaminare i tipi durante l’esecuzione. Questo approccio ti offre sicurezza dei tipi e prestazioni migliori, riducendo comunque il codice boilerplate ripetitivo.
Go fornisce il comando go generate per automatizzare la generazione del codice. Inserisci una direttiva di commento speciale nel file sorgente e, eseguendo go generate, verrà eseguito lo strumento specificato:
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Completed
)L'esecuzione di go generate ./... richiama lo strumento stringer, che crea un nuovo file contenente un metodo String() per il tipo Status. Il codice generato è normale codice Go, che viene compilato insieme al tuo programma.
Gli strumenti comuni per la generazione di codice nell'ecosistema Go includono stringer per i metodi stringa degli enum, mockgen per creare mock di test dalle interfacce e protoc per generare codice dalle definizioni di Protocol Buffer. Anche molti ORM e framework API usano la generazione per creare query al database o codice client con sicurezza dei tipi.
Il vantaggio principale rispetto alla reflection è che il codice generato viene verificato dal compilatore. Gli errori vengono rilevati durante la compilazione, non durante l'esecuzione. Il compromesso è un passaggio di compilazione aggiuntivo e la gestione dei file generati nel repository. Per i pattern OOP che richiedono implementazioni ripetitive in molti tipi, la generazione di codice offre spesso il miglior equilibrio tra sicurezza e praticità.
Sfida
FacileCostruiamo un generatore di codice che crea metodi String() per tipi simili a enum! Anche se in questo ambiente non possiamo eseguire un vero go generate, simulerai ciò che fanno gli strumenti di generazione del codice: produrre codice sorgente Go programmaticamente in base alle definizioni dei tipi.
Organizzerai il tuo codice in due file:
generator.go: crea la logica di generazione del codice.Crea una struct
EnumTypeche rappresenta la definizione di un tipo simile a enum con unName(stringa) eValues(slice di stringhe che rappresentano i nomi delle costanti).Crea una funzione
GenerateStringMethod(e EnumType) stringche produce codice sorgente Go per un metodoString(). Il codice generato dovrebbe seguire questo schema:func (e TypeName) String() string { switch e { case Value1: return "Value1" case Value2: return "Value2" default: return "Unknown" } }Crea anche una funzione
GenerateConstants(e EnumType) stringche produce le dichiarazioni delle costanti usandoiota:const ( Value1 TypeName = iota Value2 Value3 )Infine, crea
GenerateFullCode(e EnumType) stringper combinare entrambi gli output con un commento di intestazione che indica che si tratta di codice generato:// Code generated by enum generator. DO NOT EDIT. [constants] [string method]main.go: usa il tuo generatore per produrre codice per diversi tipi enum.Leggi il nome del tipo enum, quindi leggi un numero di valori seguito dal nome di ciascun valore. Passa queste informazioni al tuo generatore e stampa il codice generato completo.
Verranno forniti i seguenti input:
- Riga 1: nome del tipo enum
- Riga 2: numero di valori
- Righe successive: nome di ciascun valore
Per esempio, dato:
Status
3
Pending
Active
CompletedIl tuo output dovrebbe essere:
// 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"
}
}E dato:
Priority
2
Low
HighIl tuo output dovrebbe essere:
// 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"
}
}Questo esercizio illustra il concetto fondamentale alla base di strumenti come stringer: generare codice ripetitivo ma sicuro rispetto ai tipi in fase di compilazione, anziché usare la reflection in fase di esecuzione. Il tuo generatore produce codice Go valido che potrebbe essere salvato in un file e compilato insieme al resto di un progetto.
Provalo tu
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leggi il nome del tipo enum
typeName, _ := reader.ReadString('\n')
typeName = strings.TrimSpace(typeName)
// Leggi il numero di valori
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Leggi il nome di ogni valore
values := make([]string, count)
for i := 0; i < count; i++ {
value, _ := reader.ReadString('\n')
values[i] = strings.TrimSpace(value)
}
// TODO: Crea un EnumType con i dati letti
// TODO: Genera e stampa il codice completo usando GenerateFullCode
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti di OOP in Go
File esterniWorkspace e moduli GoPacchetti e importazioniNomi esportati e non esportatiIntroduzione all’OOP in GoGli struct come classiDefinire metodi sugli structReceiver con puntatore vs receiver con valoreInizializzazione degli structFunzioni costruttriciRiepilogo - Calcolatrice semplice4Interfacce
Introduzione alle interfacceImplementazione implicitaL’interfaccia come contrattoInterfaccia vuota (any)Asserzione di tipoSwitch sul tipoComposizione di interfacceInterfacce Stringer ed ErrorRipasso - Calcolatore di forme7Incapsulamento
Campi esportati e non esportatiIncapsulamento a livello di packageMetodi Getter e SetterNascondimento delle informazioni in GoRiepilogo - Registri degli studenti10Tipi generici (Go 1.18+)
Introduzione ai tipi genericiParametri di tipoVincoli di tipoStruct genericheSoluzione alternativa per i metodi genericiRiepilogo - Collezione generica13Modelli di progettazione Parte 1
Introduzione ai modelli di progettazioneModello SingletonModello FactoryModello Abstract FactoryModello ObserverModello Strategy2Approfondimento su tipi e struct
Tipi di base e compostiDefinizioni di tipi personalizzatiTag delle structStruct anonimeStruct annidateValori zero e predefinitiRipasso - Rubrica5Composizione invece dell'ereditarietà
Perché Go non ha l'ereditarietàFondamenti dell'embedding delle structPromozione dei metodiEmbedding di più structEmbedding e aggregazione a confrontoMascheramento dei metodi embeddedRiepilogo: gerarchia dei dipendenti8Gestione degli errori e OOP
L'interfaccia errorTipi di errore personalizzatiWrapping degli errori (fmt.Errorf)Errori sentinellaerrors.Is() ed errors.As()Panic, Defer e RecoverRiepilogo - Parser di file11Libreria standard e OOP
io.Reader e io.Writersort.InterfaceInterfaccia fmt.Stringerencoding/json con le structInterfaccia http.HandlerRipasso - modelli REST API14Modelli di progettazione – Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeMiddleware come Decorator3Puntatori e memoria
Concetti base dei puntatori in GoPuntatori a structPassaggio per valore vs per riferimentoLa funzione new()Garbage collection in GoRiepilogo - Costruttore di liste concatenate6Polimorfismo in Go
Polimorfismo tramite interfacceDuck typing in GoRegole di soddisfacimento delle interfacceCollezioni polimorficheIniezione delle dipendenzeRiepilogo - Elaboratore di pagamenti9Concorrenza e OOP
Basi delle goroutineCanali e comunicazioneCanali bufferizzati vs non bufferizzatiIstruzione selectsync.Mutex e sync.RWMutexsync.WaitGroupProgettazione di struct thread-safeRiepilogo: worker pool12Concetti avanzati di OOP
Pattern delle opzioni funzionaliPattern Builder in GoConcatenamento dei metodiAlias di tipo e definizioni a confrontoNozioni di base sulla reflectionPanoramica sulla generazione del codiceEsercitati da solo: Compilatore Go online