Go ha un sistema di tipi molto interessante. Evita classi ed eredità a favore di interfacce e composizione, ma d'altra parte non ha modelli o generici. Anche il modo in cui gestisce le collezioni è unico.
In questo tutorial imparerai a conoscere i dettagli del sistema di tipo Go e come utilizzarlo in modo efficace per scrivere codice Go chiaro e idiomatico.
Il sistema di tipo Go supporta i paradigmi procedurali, orientati agli oggetti e funzionali. Ha un supporto molto limitato per la programmazione generica. Mentre Go è un linguaggio decisamente statico, fornisce abbastanza flessibilità per le tecniche dinamiche tramite interfacce, funzioni di prima classe e riflessione. Il sistema di tipo di Go manca di funzionalità comuni nella maggior parte delle lingue moderne:
Queste omissioni sono tutte progettate per rendere Go il più semplice possibile.
Puoi creare tipi di alias in Vai e creare tipi distinti. Non è possibile assegnare un valore del tipo sottostante a un tipo con alias senza conversione. Ad esempio, il compito var b int = a
nel seguente programma provoca un errore di compilazione perché il tipo Età
è un alias di int, ma lo è non un int:
pacchetto principale tipo Age int func main () var a Age = 5 var b int = a Output: tmp / sandbox547268737 / main.go: 8: impossibile usare un (tipo Age) come tipo int in assegnazione
È possibile raggruppare dichiarazioni di tipo o utilizzare una dichiarazione per riga:
digitare IntIntMap map [int] int StringSlice [] tipo string (Size uint64 Text string CoolFunc func (a int, b bool) (int, errore))
Tutti i soliti sospetti sono qui: bool, string, interi e interi senza segno con dimensioni di bit esplicite, numeri in virgola mobile (32-bit e 64-bit) e numeri complessi (64-bit e 128-bit).
bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // alias per uint8 rune // alias per int32, rappresenta un punto di codice Unicode float32 float64 complex64 complex128
Le stringhe in Go sono codificate in UTF8 e quindi possono rappresentare qualsiasi carattere Unicode. Il pacchetto di stringhe offre un gran numero di operazioni con le stringhe. Ecco un esempio di prendere una serie di parole, convertirle in caso appropriato e unirle a una frase.
pacchetto main import ("fmt" "strings") func main () words: = [] string "i", "LikE", "the ColORS:", "RED,", "bLuE,", "AnD" , "GrEEn" properCase: = [] string per i, w: = range words if i == 0 properCase = append (properCase, strings.Title (w)) else properCase = append (properCase, stringhe.ToLower (w)) frase: = strings.Join (properCase, "") + "." fmt.Println (frase)
Go ha degli indicatori. Il puntatore nullo (vedi valori zero in seguito) è nullo. È possibile ottenere un puntatore a un valore utilizzando il &
operatore e tornare usando il *
operatore. Puoi avere anche dei puntatori ai puntatori.
pacchetto main import ("fmt") tipo S struct a float64 b stringa func main () x: = 5 px: = & x * px = 6 fmt.Println (x) ppx: = & px ** ppx = 7 fmt .Println (x)
Go supporta la programmazione orientata agli oggetti tramite interfacce e strutture. Non ci sono classi e nessuna gerarchia di classi, sebbene sia possibile incorporare strutture anonime all'interno di struct, che forniscono una sorta di singola ereditarietà.
Per un'esplorazione dettagliata della programmazione orientata agli oggetti in Go, consulta Let's Go: Programmazione orientata agli oggetti in Golang.
Le interfacce sono la pietra angolare del sistema di tipo Go. Un'interfaccia è solo una raccolta di firme dei metodi. Ogni tipo che implementa tutti i metodi è compatibile con l'interfaccia. Ecco un rapido esempio. Il Forma
l'interfaccia definisce due metodi: GetPerimeter ()
e GetArea ()
. Il Piazza
oggetto implementa l'interfaccia.
type Shape interface GetPerimeter () uint GetArea () uint tipo Square struct side uint func (s * Square) GetPerimeter () uint return s.side * 4 func (s * Square) GetArea () uint return lato sinistro * lato
L'interfaccia vuota interfaccia
è compatibile con qualsiasi tipo perché non ci sono metodi che sono richiesti. L'interfaccia vuota può quindi puntare a qualsiasi oggetto (simile al puntatore void di Object o C / C ++ di Java) e viene spesso utilizzato per la digitazione dinamica. Le interfacce sono sempre puntatori e indicano sempre un oggetto concreto.
Per un intero articolo sulle interfacce Go, consulta: Come definire e implementare un'interfaccia Go.
Le strutture sono tipi definiti dall'utente di Go. Una struttura contiene campi denominati, che possono essere tipi di base, tipi di puntatori o altri tipi di struttura. È inoltre possibile incorporare le strutture in modo anonimo in altre strutture come forma di ereditarietà di implementazione.
Nell'esempio seguente, le strutture S1 e S2 sono incorporate nella struttura S3, che ha anche il suo int
campo e un puntatore al proprio tipo:
pacchetto main import ("fmt") tipo S1 struct f1 int tipo S2 struct f2 int tipo S3 struct S1 S2 f3 int f4 * S3 func main () s: = & S3 S1 5, S2 6, 7, nil fmt.Println (s) Output: & 5 6 7
Le asserzioni di tipo consentono di convertire un'interfaccia nel suo tipo concreto. Se conosci già il tipo sottostante, puoi semplicemente affermarlo. Se non sei sicuro, puoi provare diverse asserzioni di tipi finché non trovi il tipo giusto.
Nell'esempio seguente, c'è una lista di cose che contengono stringhe e valori non stringa rappresentati come una fetta di interfacce vuote. Il codice itera su tutte le cose, cerca di convertire ogni elemento in una stringa e memorizza tutte le stringhe in una sezione separata che alla fine stampa.
pacchetto principale import "fmt" func main () cose: = [] interface "hi", 5, 3.8, "there", nil, "!" stringhe: = [] stringa per _, t : = range things s, ok: = t. (stringa) se ok string = append (stringhe, s) fmt.Println (stringhe) Output: [Ciao!]
The Go riflettere
pacchetto ti permette di controllare direttamente il tipo di un'interfaccia senza le asserzioni di tipo. Puoi anche estrarre il valore di un'interfaccia e convertirla in un'interfaccia se lo desideri (non altrettanto utile).
Ecco un esempio simile all'esempio precedente, ma invece di stampare le stringhe, le conteggia solo, quindi non c'è bisogno di convertire da interfaccia
a stringa
. La chiave sta chiamando reflect.Type ()
per ottenere un oggetto di tipo, che ha a Genere()
metodo che ci permette di rilevare se abbiamo a che fare con una stringa o meno.
pacchetto main import ("fmt" "reflect") func main () cose: = [] interface "hi", 5, 3.8, "there", nil, "!" stringCount: = 0 per _, t: = range things tt: = reflect.TypeOf (t) iftt! = nil && tt.Kind () == reflect.String stringCount ++ fmt.Println ("Conteggio stringhe:", stringCount)
Le funzioni sono cittadini di prima classe in Go. Ciò significa che è possibile assegnare funzioni a variabili, passare funzioni come argomenti ad altre funzioni o restituirle come risultati. Ciò ti consente di utilizzare lo stile di programmazione funzionale con Go.
L'esempio seguente mostra un paio di funzioni, GetUnaryOp ()
e GetBinaryOp ()
, che restituiscono funzioni anonime selezionate casualmente. Il programma principale decide se ha bisogno di un'operazione unaria o di un'operazione binaria basata sul numero di argomenti. Memorizza la funzione selezionata in una variabile locale chiamata "op" e quindi la invoca con il numero corretto di argomenti.
pacchetto main import ("fmt" "math / rand") tipo UnaryOp func (a int) int tipo BinaryOp func (a, b int) int func GetBinaryOp () BinaryOp if rand.Intn (2) == 0 return func (a, b int) int return a + b else return func (a, b int) int return a - b func GetUnaryOp () UnaryOp if rand.Intn (2) == 0 return func (a int) int return -a else return func (a int) int return a * a func main () argomenti: = [] [] int 4,5, 6, 9, 7,18, 33 var risultato int per _, a: = argomenti range if len (a) == 1 op: = GetUnaryOp () result = op (a [0]) else op: = GetBinaryOp () result = op (a [0], a [1]) fmt.Println (risultato)
I canali sono un tipo di dati insolito. Puoi considerarli come code di messaggi usate per passare messaggi tra goroutine. I canali sono fortemente tipizzati. Sono sincronizzati e dispongono di supporto di sintassi dedicato per l'invio e la ricezione di messaggi. Ogni canale può essere solo ricezione, solo invio o bidirezionale.
I canali possono anche essere opzionalmente bufferizzati. È possibile scorrere i messaggi di un canale utilizzando l'intervallo e le routine di go possono bloccarsi su più canali contemporaneamente utilizzando l'operazione di selezione versatile.
Ecco un tipico esempio in cui la somma dei quadrati di una lista di interi è calcolata in parallelo da due routine di go, ognuna responsabile della metà della lista. La funzione principale attende i risultati di entrambe le routine di go e quindi somma le somme parziali per il totale. Nota come il canale c
viene creato utilizzando il rendere()
funzione built-in e come il codice legge e scrive sul canale tramite lo speciale <-
operatore.
pacchetto principale import "fmt" func sum_of_squares (s [] int, c chan int) somma: = 0 per _, v: = intervallo s somma + = v * v c <- sum // send sum to c func main() s := []int11, 32, 81, -9, -14 c := make(chan int) go sum_of_squares(s[:len(s)/2], c) go sum_of_squares(s[len(s)/2:], c) sum1, sum2 := <-c, <-c // receive from c total := sum1 + sum2 fmt.Println(sum1, sum2, total)
Questo è solo raschiando la superficie. Per una revisione dettagliata dei canali, consulta:
Go ha diverse raccolte generiche integrate che possono archiviare qualsiasi tipo. Queste raccolte sono speciali e non è possibile definire le proprie raccolte generiche. Le raccolte sono matrici, sezioni e mappe. Anche i canali sono generici e possono essere considerati anche collezioni, ma hanno alcune proprietà piuttosto uniche, quindi preferisco parlarne separatamente.
Le matrici sono raccolte di elementi di dimensioni fisse dello stesso tipo. Ecco alcuni array:
pacchetto principale import "fmt" func main () a1: = [3] int 1, 2, 3 var a2 [3] int a2 = a1 fmt.Println (a1) fmt.Println (a2) a1 [1] = 7 fmt.Println (a1) fmt.Println (a2) a3: = [2] interfaccia 3, "ciao" fmt.Println (a3)
La dimensione dell'array fa parte del suo tipo. È possibile copiare array dello stesso tipo e dimensione. La copia è in base al valore. Se si desidera memorizzare elementi di diverso tipo, è possibile utilizzare il pannello di escape di una serie di interfacce vuote.
Gli array sono piuttosto limitati a causa delle loro dimensioni fisse. Le fette sono molto più interessanti. Puoi pensare a fette come array dinamici. Sotto le coperte, le fette usano una matrice per la memorizzazione dei loro elementi. Puoi controllare la lunghezza di una sezione, aggiungere elementi e altre sezioni e, più divertente di tutte, puoi estrarre sottosequenze simili a quelle di Python:
pacchetto main import "fmt" func main () s1: = [] int 1, 2, 3 var s2 [] int s2 = s1 fmt.Println (s1) fmt.Println (s2) // Modifica s1 s1 [ 1] = 7 // Sia s1 che s2 puntano allo stesso array sottostante fmt.Println (s1) fmt.Println (s2) fmt.Println (len (s1)) // Slice s1 s3: = s1 [1: len ( s1)] fmt.Println (s3)
Quando copi le porzioni, copia il riferimento sullo stesso array sottostante. Quando si taglia, la sottosquadra punta ancora allo stesso array. Ma quando si aggiunge, si ottiene una porzione che punta a una nuova matrice.
È possibile eseguire iterazioni su array o slice utilizzando un ciclo regolare con indici o utilizzando intervalli. Puoi anche creare sezioni in una determinata capacità che verranno inizializzate con il valore zero del loro tipo di dati utilizzando il rendere()
funzione:
pacchetto principale import "fmt" func main () // Crea una porzione di 5 booleani inizializzati su false s1: = make ([] bool, 5) fmt.Println (s1) s1 [3] = true s1 [4] = true fmt.Println ("Iterate using standard for loop with index") per i: = 0; io < len(s1); i++ fmt.Println(i, s1[i]) fmt.Println("Iterate using range") for i, x := range(s1) fmt.Println(i, x) Output: [false false false false false] Iterate using standard for loop with index 0 false 1 false 2 false 3 true 4 true Iterate using range 0 false 1 false 2 false 3 true 4 true
Le mappe sono raccolte di coppie chiave-valore. Puoi assegnare loro letterali di mappa o altre mappe. Puoi anche creare mappe vuote usando il rendere
funzione integrata. Si accede agli elementi usando parentesi quadre. Le mappe supportano l'iterazione usando gamma
, e puoi verificare se esiste una chiave provando ad accedervi e controllando il secondo valore di ritorno booleano opzionale.
pacchetto main import ("fmt") func main () // Crea mappa usando una mappa letterale m: = map [int] string 1: "one", 2: "two", 3: "three" // Assegna all'elemento con il tasto m [5] = "cinque" // Accedi all'elemento con il tasto fmt.Println (m [2]) v, ok: = m [4] se ok fmt.Println (v) else fmt .Println ("Chiave mancante: 4") per k, v: = intervallo m fmt.Println (k, ":", v) Output: due chiavi mancanti: 4 5: cinque 1: uno 2: due 3: tre
Si noti che l'iterazione non è in fase di creazione o ordine di inserimento.
Non ci sono tipi non inizializzati in Go. Ogni tipo ha un valore zero predefinito. Se una variabile di un tipo viene dichiarata senza assegnargli un valore, allora contiene il suo valore zero. Questa è una caratteristica importante di sicurezza del tipo.
Per qualsiasi tipo T
, *tritone)
restituirà un valore zero di T
.
Per i tipi booleani, il valore zero è "falso". Per i tipi numerici, il valore zero è ... zero. Per fette, mappe e puntatori, è zero. Per le strutture, è una struttura in cui tutti i campi sono inizializzati al loro valore zero.
pacchetto main import ("fmt") tipo S struct a float64 b stringa func main () fmt.Println (* new (bool)) fmt.Println (* new (int)) fmt.Println (* new ([nuovo (int)) ] string)) fmt.Println (* new (map [int] string)) x: = * new ([] string) se x == nil fmt.Println ("Le sezioni non inizializzate sono nil") y: = * nuovo (map [int] string) if y == nil fmt.Println ("Anche le mappe non inizializzate sono nil") fmt.Println (* new (S))
Go non ne ha. Questa è probabilmente la lamentela più comune sul sistema di tipo di Go. I progettisti di Go sono aperti all'idea, ma non sanno ancora come implementarla senza violare gli altri principi di progettazione alla base del linguaggio. Cosa puoi fare se hai bisogno di alcuni tipi di dati generici? Ecco alcuni suggerimenti:
Go ha un sistema di tipo interessante. I progettisti di Go hanno preso decisioni esplicite per rimanere sul lato semplice dello spettro. Se sei serio sulla programmazione di Go, dovresti investire il tempo e conoscere il suo sistema di tipi e le sue idiosincrasie. Ne valuterà il tuo tempo.