Condivisione della tecnologia

[Domanda nell'intervista] Golang (Parte 4)

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Sommario

1.La differenza tra marca e nuovo

2. Come utilizzare il trasferimento di valore e il trasferimento di indirizzo (trasferimento di riferimento) nella lingua Go? Qual è la differenza?

3. Qual è la differenza tra array e slice nel linguaggio Go? Qual è la differenza tra array e sezioni quando si passa in linguaggio Go? In che modo il linguaggio Go implementa l'espansione delle sezioni?

4.Cos'è Go Convey? A cosa serve generalmente?

5.Quali sono le funzioni e le caratteristiche del differimento?

6.L'implementazione alla base di Go Slice? Vai al meccanismo di espansione delle fette?

7. Le fette sono le stesse prima e dopo l'espansione?

8.Perché Slice non è thread-safe?


1.La differenza tra marca e nuovo

Differenze: 1. Make può essere utilizzato solo per allocare e inizializzare dati di tipo slice, map e chan mentre new può allocare qualsiasi tipo di dati;

2. La nuova allocazione restituisce un puntatore, che è il tipo "*Type" mentre make restituisce un riferimento, che è Type.

3. Lo spazio allocato da new verrà cancellato; dopo che make avrà allocato lo spazio, verrà inizializzato.

2. Come utilizzare il trasferimento di valore e il trasferimento di indirizzo (trasferimento di riferimento) nella lingua Go? Qual è la differenza?

Il passaggio per valore copierà solo il valore del parametro e lo inserirà nella funzione corrispondente. Gli indirizzi delle due variabili sono diversi e non possono modificarsi a vicenda.

Il passaggio dell'indirizzo (passaggio per riferimento) passerà la variabile stessa alla funzione corrispondente e il contenuto del valore della variabile potrà essere modificato nella funzione.

3. Qual è la differenza tra array e slice nel linguaggio Go? Qual è la differenza tra array e sezioni quando si passa in linguaggio Go? In che modo il linguaggio Go implementa l'espansione delle sezioni?

Vettore:

Lunghezza fissa dell'array La lunghezza dell'array fa parte del tipo di array, quindi [3]int e [4]int sono due tipi di array diversi. È necessario specificare la dimensione dell'array. Se non specificata, la dimensione verrà calcolata automaticamente in base a coppia di inizializzazione. L'array non può essere modificato viene passato per valore

fetta:

La lunghezza di una sezione può essere modificata. Una sezione è una struttura dati leggera, puntatore, lunghezza e capacità, non è necessario specificarne la dimensione. Le porzioni vengono passate per indirizzo (passaggio per riferimento) e possono essere inizializzate tramite un array o tramite la funzione incorporata make(). Durante l'inizializzazione, len=cap viene quindi espanso.

Le matrici sono tipi di valore e le sezioni sono tipi di riferimento;

La lunghezza degli array è fissa, ma le sezioni no (le sezioni sono array dinamici)

Lo strato sottostante della sezione è un array

Il meccanismo di espansione delle sezioni del linguaggio Go è molto intelligente: ottiene l'espansione riallocando l'array sottostante e migrando i dati in un nuovo array. Nello specifico, quando è necessario espandere una sezione, il linguaggio Go crea un nuovo array sottostante e copia i dati dell'array originale nel nuovo array. Quindi, il puntatore della sezione punta al nuovo array, la lunghezza viene aggiornata alla lunghezza originale più la lunghezza espansa e la capacità viene aggiornata alla lunghezza del nuovo array.

Per ottenere l'espansione della sezione:

andare1.17

  1. // src/runtime/slice.go
  2. func growslice(et *_type, old slice, cap int) slice {
  3.     // ...
  4.     newcap := old.cap
  5.     doublecap := newcap + newcap
  6.     if cap > doublecap {
  7.         newcap = cap
  8.     } else {
  9.         if old.cap < 1024 {
  10.             newcap = doublecap
  11.         } else {
  12.             // Check 0 < newcap to detect overflow
  13.             // and prevent an infinite loop.
  14.             for 0 < newcap && newcap < cap {
  15.                 newcap += newcap / 4
  16.             }
  17.             // Set newcap to the requested cap when
  18.             // the newcap calculation overflowed.
  19.             if newcap <= 0 {
  20.                 newcap = cap
  21.             }
  22.         }
  23.     }
  24.     // ...
  25.     return slice{p, old.len, newcap}
  26. }

Prima di allocare lo spazio di memoria, è necessario determinare la nuova capacità della slice. In fase di esecuzione è possibile selezionare diverse strategie di espansione in base alla capacità attuale della slice:

Se la capacità prevista è maggiore del doppio della capacità attuale, verrà utilizzata la capacità prevista, se la lunghezza dello slice attuale è inferiore a 1024, la capacità verrà raddoppiata se la lunghezza dello slice attuale è maggiore o uguale; 1024, la capacità verrà aumentata ogni volta del 25% fino alla nuova capacità Capacità superiore a quella prevista;

andare1.18

  1. // src/runtime/slice.go
  2. func growslice(et *_type, old slice, cap int) slice {
  3.     // ...
  4.     newcap := old.cap
  5.     doublecap := newcap + newcap
  6.     if cap > doublecap {
  7.         newcap = cap
  8.     } else {
  9.         const threshold = 256
  10.         if old.cap < threshold {
  11.             newcap = doublecap
  12.         } else {
  13.             // Check 0 < newcap to detect overflow
  14.             // and prevent an infinite loop.
  15.             for 0 < newcap && newcap < cap {
  16.                 // Transition from growing 2x for small slices
  17.                 // to growing 1.25x for large slices. This formula
  18.                 // gives a smooth-ish transition between the two.
  19.                 newcap += (newcap + 3*threshold) / 4
  20.             }
  21.             // Set newcap to the requested cap when
  22.             // the newcap calculation overflowed.
  23.             if newcap <= 0 {
  24.                 newcap = cap
  25.             }
  26.         }
  27.     }
  28.     // ...
  29.     return slice{p, old.len, newcap}
  30. }

La differenza rispetto alla versione precedente sta principalmente nella soglia di espansione e in questa riga di codice:newcap += (newcap + 3*threshold) / 4

Prima di allocare lo spazio di memoria, è necessario determinare la nuova capacità della slice. In fase di esecuzione è possibile selezionare diverse strategie di espansione in base alla capacità attuale della slice:

  • Se la capacità prevista è superiore al doppio della capacità attuale, verrà utilizzata la capacità prevista;

  • Se la lunghezza della slice corrente è inferiore alla soglia (default 256), la capacità verrà raddoppiata;

  • Se la lunghezza della sezione corrente è maggiore o uguale alla soglia (predefinita 256), la capacità verrà aumentata del 25% ogni volta newcap + 3*threshold, finché la nuova capacità non sarà maggiore della capacità prevista;

L'espansione degli slice è divisa in due fasi, prima e dopo go1.18:

1. Prima dell'1.18:

  • Se la capacità prevista è superiore al doppio della capacità attuale, verrà utilizzata la capacità prevista;

  • Se la lunghezza della sezione corrente è inferiore a 1024, la capacità verrà raddoppiata;

  • Se la lunghezza della sezione corrente è maggiore di 1024, la capacità verrà aumentata ogni volta del 25% finché la nuova capacità non sarà maggiore della capacità prevista;

2. Dopo l'1.18:

  • Se la capacità prevista è superiore al doppio della capacità attuale, verrà utilizzata la capacità prevista;

  • Se la lunghezza della slice corrente è inferiore alla soglia (default 256), la capacità verrà raddoppiata;

  • Se la lunghezza della sezione corrente è maggiore o uguale alla soglia (predefinita 256), la capacità verrà aumentata del 25% ogni volta newcap + 3*threshold, finché la nuova capacità non sarà maggiore della capacità prevista;

4.Cos'è Go Convey? A cosa serve generalmente?

go convogliare è un framework di test unitario che supporta Golang

go trasmettere può monitorare automaticamente le modifiche ai file e avviare i test e può inviare i risultati dei test all'interfaccia web in tempo reale

go trasmettere fornisce ricche asserzioni per semplificare la scrittura dei casi di test

5.Quali sono le funzioni e le caratteristiche del differimento?

La funzione del differimento è:

È sufficiente aggiungere la parola chiave defer prima di chiamare una normale funzione o metodo per completare la sintassi richiesta per defer. Quando viene eseguita l'istruzione defer, la funzione successiva al defer verrà rinviata. La funzione dopo il defer non verrà eseguita finché non viene eseguita la funzione contenente l'istruzione defer, indipendentemente dal fatto che la funzione contenente l'istruzione defer termini normalmente tramite return o termini in modo anomalo a causa di panico. È possibile eseguire più istruzioni di differimento in una funzione, nell'ordine inverso rispetto alla loro dichiarazione.

Scenari comuni per il differimento:

L'istruzione defer viene spesso utilizzata per gestire operazioni abbinate, come apertura, chiusura, connessione, disconnessione, blocco e rilascio dei blocchi.

Attraverso il meccanismo di differimento, non importa quanto complessa sia la logica della funzione, è possibile garantire che le risorse vengano rilasciate in qualsiasi percorso di esecuzione.

 

6.L'implementazione alla base di Go Slice? Vai al meccanismo di espansione delle fette?

L'affettamento è implementato in base agli array. Il suo livello sottostante è un array. Esso stesso è molto piccolo e può essere inteso come un'astrazione dell'array sottostante. Poiché è implementato in base agli array, la memoria sottostante viene allocata in modo continuo, il che è molto efficiente. I dati possono essere ottenuti anche tramite indici e possono essere iterati e ottimizzati per la raccolta dei rifiuti. Le porzioni stesse non sono array dinamici o puntatori di array. La struttura dati implementata internamente fa riferimento all'array sottostante tramite un puntatore e gli attributi rilevanti sono impostati per limitare le operazioni di lettura e scrittura dei dati a un'area specifica. La slice stessa è un oggetto di sola lettura e il suo meccanismo di funzionamento è simile all'incapsulamento di un puntatore ad array.

L'oggetto slice è molto piccolo perché è una struttura dati con solo 3 campi:

puntatore all'array sottostante

lunghezza della fetta

capacità delle fette

La strategia per suddividere l'espansione in Go (1.17) è la seguente:

Primo giudice, se la nuova capacità applicata è maggiore di 2 volte la vecchia capacità, la capacità finale sarà la nuova capacità applicata.

Altrimenti, se la lunghezza della vecchia sezione è inferiore a 1024, la capacità finale sarà doppia rispetto alla vecchia capacità.

Diversamente, se la lunghezza della vecchia slice è maggiore o uguale a 1024, la capacità finale verrà incrementata ciclicamente di 1/4 dalla vecchia capacità finché la capacità finale non sarà maggiore o uguale alla nuova capacità richiesta.

Se il valore del calcolo della capacità finale eccede, la capacità finale sarà la nuova capacità richiesta.

7. Le fette sono le stesse prima e dopo l'espansione?

Situazione uno:

L'array originale ha ancora una capacità che può essere espansa (la capacità effettiva non è stata riempita). In questo caso, l'array espanso punta ancora all'array originale. L'operazione di una sezione può influenzare più puntatori che puntano allo stesso indirizzo del file Fetta.

Situazione due:

Risulta che la capacità dell'array ha raggiunto il valore massimo. Se desideri espandere la capacità, Go aprirà prima un'area di memoria per impostazione predefinita, copierà il valore originale e quindi eseguirà l'operazione append(). Questa situazione non influisce affatto sull'array originale. Per copiare una Slice è meglio utilizzare la funzione Copia.

8.Perché Slice non è thread-safe?

slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,
使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致;
slice在并发执行中不会报错,但是数据会丢失
​
如果想实现slice线程安全,有两种方式:
​
方式一:通过加锁实现slice线程安全,适合对性能要求不高的场景。

  1. func TestSliceConcurrencySafeByMutex(t *testing.T) {
  2. var lock sync.Mutex //互斥锁
  3. a := make([]int, 0)
  4. var wg sync.WaitGroup
  5. for i := 0; i < 10000; i++ {
  6.  wg.Add(1)
  7.  go func(i int) {
  8.   defer wg.Done()
  9.   lock.Lock()
  10.   defer lock.Unlock()
  11.   a = append(a, i)
  12. }(i)
  13. }
  14. wg.Wait()
  15. t.Log(len(a))
  16. // equal 10000
  17. }
方式二:通过channel实现slice线程安全,适合对性能要求高的场景。
  1. func TestSliceConcurrencySafeByChanel(t *testing.T) {
  2. buffer := make(chan int)
  3. a := make([]int, 0)
  4. // 消费者
  5. go func() {
  6.  for v := range buffer {
  7.   a = append(a, v)
  8. }
  9. }()
  10. // 生产者
  11. var wg sync.WaitGroup
  12. for i := 0; i < 10000; i++ {
  13.  wg.Add(1)
  14.  go func(i int) {
  15.   defer wg.Done()
  16.   buffer <- i
  17. }(i)
  18. }
  19. wg.Wait()
  20. t.Log(len(a))
  21. // equal 10000
  22. }