Preparazione per Go Junior

Domande da Colloquio per Sviluppatori Backend Go Junior

15 domande da colloquio selezionate su Go per sviluppatori backend Junior che devono spiegare i concetti fondamentali con chiarezza e sicurezza.

Inizia un Colloquio AI per Go JuniorNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Sistema di Tipi

1Spiega come funzionano i valori zero (zero value) di Go per i tipi integrati e di tipo riferimento, e perché sono importanti quando si dichiarano variabili senza un'inizializzazione esplicita.

In Go, una variabile dichiarata senza un inizializzatore esplicito viene automaticamente inizializzata al valore zero del suo tipo. I tipi numerici diventano 0, bool diventa false, string diventa "", e array o struct vengono azzerati elemento per elemento o campo per campo. I tipi simili a puntatori o riferimenti, come puntatori, slice, map, channel, funzioni e interfacce, hanno nil come valore zero. Questo è importante perché le variabili Go e i campi omessi di una struct iniziano in uno stato deterministico anziché contenere dati spazzatura, e molte API sono progettate in modo che il valore zero sia un'impostazione predefinita utile, sebbene alcuni valori nil richiedano comunque un'inizializzazione prima di eseguire determinate operazioni.

Prova a rispondere a questa domanda con un coach IA

2Come gestisce Go l'uguaglianza per le `struct` e cosa succede quando una `struct` contiene campi non comparabili?

I valori di tipo `struct` in Go possono essere confrontati tramite `==` e `!=` solo quando ogni campo della `struct` è comparabile. L'uguaglianza confronta i campi corrispondenti utilizzando la regola di uguaglianza specifica di ciascun campo. Se una `struct` contiene un campo non comparabile come una `slice`, una `map` o una funzione, il tipo `struct` non è comparabile e il confronto di due valori di quel tipo `struct` con `==` genera un errore a tempo di compilazione. Per tali `struct`, è necessario utilizzare una logica di confronto personalizzata o una funzione di supporto adeguata per l'uguaglianza profonda (deep-equality), specialmente nei test.

Prova a rispondere a questa domanda con un coach IA

3Spiega l'immutabilità delle stringhe in Go e la relazione tra string, []byte, bytes.Buffer e strings.Builder.

In Go, una `string` è una sequenza immutabile di byte, spesso testo UTF-8, sebbene non sia richiesto che sia UTF-8 valido. Non è possibile modificare una stringa sul posto; per alterarne il contenuto, in genere la si converte in `[]byte` per modifiche a livello di byte o in `[]rune` per modifiche a livello di code point, per poi convertirla nuovamente. Le normali conversioni tra `string` e `[]byte` copiano i dati e possono comportare allocazioni, perciò le conversioni ripetute o le concatenazioni continue all'interno di cicli possono risultare costose. `strings.Builder` è ottimizzato per costruire stringhe in modo efficiente, mentre `bytes.Buffer` è un buffer di byte mutabile utile per dati orientati ai byte e per l'I/O, ed è anche in grado di produrre una stringa.

Prova a rispondere a questa domanda con un coach IA

4Come funzionano i puntatori in Go e quali operazioni sono intenzionalmente non consentite rispetto al C?

Un puntatore in Go è un valore tipizzato che contiene l'indirizzo di un altro valore, come `*int` che punta a un `int`. Si usa `&x` per ottenere l'indirizzo di un valore e `*p` per dereferenziare un puntatore per leggere o scrivere il valore puntato. Passare o memorizzare un puntatore permette a più parti del codice di osservare o mutare lo stesso valore sottostante, e un puntatore può essere `nil`. A differenza del C, il codice Go sicuro non consente intenzionalmente l'aritmetica dei puntatori e la manipolazione arbitraria di indirizzi grezzi; le eccezioni di basso livello richiedono l'uso del pacchetto `unsafe`.

Prova a rispondere a questa domanda con un coach IA

5Spiega come funzionano le costanti e iota in Go, incluse le costanti tipizzate rispetto a quelle non tipizzate.

Le costanti in Go sono valori definiti a tempo di compilazione: booleani, stringhe o numerici. Una costante tipizzata possiede un tipo specifico. Una costante non tipizzata non ha un tipo concreto fisso finché non viene usata in un contesto tipizzato; le costanti numeriche non tipizzate sono rappresentate in modo esatto o con alta precisione fino a quel momento e il valore deve poter essere rappresentabile nel tipo scelto. `iota` è un identificatore pre-dichiarato utilizzato nelle dichiarazioni `const`: inizia da 0 in ogni blocco `const` e si incrementa per ogni specifica di costante, rendendolo utile per definire costanti simili a enum o flag di bit.

Prova a rispondere a questa domanda con un coach IA

6Come funzionano le conversioni esplicite in Go, e perché il linguaggio evita molte conversioni implicite?

Go richiede generalmente conversioni esplicite utilizzando `T(x)` quando si cambia il tipo di un valore, come `int64(i)` o `MyID(n)`. Questo mantiene visibili i cambiamenti di tipo ed evita sorprese dovute a conversioni implicite numeriche, booleane o di stringhe. Le conversioni sono consentite solo in base alle regole di conversione di Go e possono modificare il valore, ad esempio attraverso l'overflow o il troncamento di interi, il troncamento da float a intero verso lo zero, o la perdita di precisione. Le costanti non tipizzate sono più flessibili: possono essere utilizzate in un contesto tipizzato se il valore della costante è rappresentabile in quel tipo.

Prova a rispondere a questa domanda con un coach IA

Strutture Dati

7Descrivi la differenza tra array e slice in Go, includendo il comportamento della lunghezza, della capacità e della memoria sottostante.

Un array in Go ha una lunghezza fissa che fa parte del suo tipo, come [3]int; memorizza i suoi elementi direttamente e l'assegnazione o il passaggio di un array copia l'intero valore dell'array. Una slice, come []int, è un piccolo descrittore di un array sottostante: concettualmente contiene un puntatore agli elementi, una lunghezza e una capacità. La lunghezza di una slice è il numero di elementi visibili; la sua capacità è quanti elementi possono essere utilizzati dall'inizio della slice prima di raggiungere la fine dell'array sottostante. Le slice sono flessibili: il reslicing modifica il descrittore e l'operazione di append può riutilizzare lo stesso array sottostante se la capacità lo consente, oppure allocarne uno nuovo in caso contrario.

Prova a rispondere a questa domanda con un coach IA

8Come si comporta il tipo `map` in Go per quanto riguarda i tipi di chiave, le chiavi mancanti, le `map` nulle (`nil`) e l'ordine di iterazione?

I tipi di chiave delle `map` in Go devono essere confrontabili; le `slice`, le `map` e le funzioni non possono essere usate direttamente come chiavi. La ricerca di una chiave non presente restituisce il valore zero del tipo di elemento, pertanto si utilizza l'idioma comma-ok (`v, ok := m[k]`) per distinguere l'assenza della chiave da un valore zero effettivamente presente. È possibile leggere e iterare su una `map` a `nil`, ma l'assegnazione causa un panic; deve essere inizializzata prima della scrittura. L'ordine di iterazione della `map` non è specificato e il codice non deve dipendere da esso.

Prova a rispondere a questa domanda con un coach IA

Gestione della Memoria

9Quando si dovrebbe passare una `struct` o un altro valore per valore rispetto al passaggio per puntatore in Go?

Passa per valore quando il valore è piccolo, in sola lettura per la chiamata, o quando desideri una semantica di copia indipendente. Passa per puntatore quando la funzione deve mutare il valore del chiamante, la copia risulterebbe costosa, il tipo non dovrebbe essere copiato, o l'uso di `nil` o un'identità condivisa fa parte dell'API. Non dare per scontato che i puntatori siano sempre più veloci: possono introdurre aliasing, allocazioni sull'heap (heap escape), lavoro per il GC (Garbage Collector) e accessi meno ottimizzati per la cache. Preferisci in primo luogo una semantica chiara ed effettua benchmark per le scelte critiche per le prestazioni.

Prova a rispondere a questa domanda con un coach IA

Semantica del Linguaggio

10Come si comportano l'assegnazione e la copia per `struct`, array, `map` e `slice` in Go?

L'assegnazione in Go copia il valore assegnato, ma l'effetto dipende dal tipo. L'assegnazione di una `struct` ne copia i campi, e l'assegnazione di un array ne copia ogni elemento. L'assegnazione di una `slice` copia l'intestazione della `slice` (puntatore, lunghezza e capacità), quindi di solito le `slice` condividono lo stesso array sottostante. L'assegnazione di una `map` copia un descrittore simile a un riferimento, in modo che entrambe le variabili puntino agli stessi dati della mappa. Per ottenere una proprietà indipendente, è necessario effettuare una copia difensiva, ad esempio utilizzando `copy` o `slices.Clone` per le `slice` e creando una nuova `map` in cui copiare le singole voci per le mappe.

Prova a rispondere a questa domanda con un coach IA

11In che modo le closure catturano le variabili in Go, e quali bug possono presentarsi in relazione a cicli e goroutine?

Una closure in Go è un valore di tipo funzione che fa riferimento a variabili del suo ambito (scope) lessicale circostante. Cattura le variabili stesse (per riferimento), permettendo così alla closure di osservarne le mutazioni; se la closure sfugge al suo ambito di creazione (escape), le variabili catturate possono vivere più a lungo della funzione che le ha create. I bug relativi a cicli e goroutine si verificano quando le closure condividono o osservano una variabile dopo che questa è stata modificata, portando spesso le goroutine o le callback ritardate a leggere un valore non voluto o causando data race su uno stato condiviso. La soluzione più comune consiste nel passare il valore desiderato alla closure come argomento oppure creare una nuova copia locale della variabile per ogni iterazione, sincronizzando l'accesso concorrente ove necessario. A partire da Go 1.22, le variabili di ciclo dichiarate tramite `for`/`range` sono specifiche per singola iterazione, il che elimina gran parte dei classici bug di cattura delle variabili, tuttavia le variabili riutilizzate all'esterno del ciclo o altro stato condiviso mutabile possono continuare a causare problemi.

Prova a rispondere a questa domanda con un coach IA

12Descrivi la semantica di `range` su array, slice, map, stringhe e channel.

L'istruzione `range` itera in base al tipo di operando. Su un array o una slice restituisce un indice e il valore dell'elemento; il valore dell'elemento è una copia, quindi l'assegnazione a quest'ultimo non modifica la collezione. Iterare su un valore array copia l'array per l'iterazione, mentre iterare su una slice utilizza l'intestazione della slice e indicizza l'array sottostante. Su una map restituisce chiave e valore in un ordine non specificato, e il valore è una copia. Su una stringa restituisce l'indice del byte e il code point Unicode decodificato (`rune`), non un indice di posizione della runa. Su un channel riceve valori fino a quando il channel non viene chiuso e svuotato; eseguire un `range` su un channel `nil` blocca l'esecuzione per sempre.

Prova a rispondere a questa domanda con un coach IA

Metodi

13Qual è la differenza tra ricevitori di tipo valore (value receiver) e ricevitori di tipo puntatore (pointer receiver), e in che modo la scelta del ricevitore influisce sulla mutabilità, sulla copia e sul soddisfacimento delle interfacce?

Un metodo con ricevitore di tipo valore riceve una copia del ricevitore, quindi è adatto per comportamenti di sola lettura e per tipi piccoli in stile immutabile; le modifiche alla copia del ricevitore non sono visibili al chiamante. Un metodo con ricevitore di tipo puntatore riceve una copia di un puntatore al ricevitore, quindi può mutare l'originale ed evitare la copia di valori grandi o che non dovrebbero essere copiati. La scelta del ricevitore influisce sui method set: i metodi con ricevitori di tipo valore sono nel method set sia di T che di *T, mentre i metodi con ricevitori di tipo puntatore sono solo nel method set di *T. Pertanto, un'interfaccia che richiede un metodo con ricevitore di tipo puntatore è soddisfatta da *T, non da T, anche se un valore T indirizzabile può chiamare il metodo utilizzando la normale sintassi di chiamata dei metodi.

Prova a rispondere a questa domanda con un coach IA

Interfacce

14Descrivi come funzionano le interfacce in Go e cosa significa che un tipo soddisfa un'interfaccia in modo implicito.

Un'interfaccia in Go definisce un insieme di metodi richiesti. Un tipo concreto soddisfa un'interfaccia implicitamente quando il suo insieme di metodi (method set) contiene quei metodi; non c'è alcuna dichiarazione esplicita di implementazione. Una variabile di tipo interfaccia può contenere un valore dinamico concreto il cui tipo soddisfa l'interfaccia, e le chiamate ai metodi dell'interfaccia vengono instradate all'implementazione di tale valore dinamico. L'interfaccia vuota, scritta come `interface{}` o solitamente `any`, non ha metodi richiesti, quindi ogni tipo la soddisfa. Il fatto che `T`, `*T` o entrambi soddisfino un'interfaccia dipende dai rispettivi insiemi di metodi e dalle scelte del ricevitore (receiver).

Prova a rispondere a questa domanda con un coach IA

15Come funzionano le asserzioni di tipo (type assertion) e gli switch di tipo (type switch) con i valori di interfaccia?

Un'asserzione di tipo verifica il valore dinamico memorizzato in un'interfaccia: `v := x.(T)` ha successo se il tipo dinamico del valore dell'interfaccia è `T`, o se `T` è un'interfaccia implementata dal valore dinamico. La forma a risultato singolo genera un panic in caso di fallimento. La forma comma-ok, `v, ok := x.(T)`, segnala il successo senza generare panic. Uno switch di tipo, `switch v := x.(type)`, dirama l'esecuzione in base al tipo dinamico del valore dell'interfaccia, assegnando a `v` il tipo corrispondente al caso soddisfatto.

Prova a rispondere a questa domanda con un coach IA