Partage de technologie

[Question d'entretien] Golang (Partie 4)

2024-07-12

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

Table des matières

1. La différence entre marque et nouveau

2. Comment utiliser le transfert de valeur et le transfert d'adresse (transfert de référence) en langage Go ? Quelle est la différence?

3. Quelle est la différence entre les tableaux et les tranches en langage Go ? Quelle est la différence entre les tableaux et les tranches lors du passage en langage Go ? Comment le langage Go implémente-t-il l’expansion des tranches ?

4. Qu’est-ce que Go Convey ? A quoi sert-il généralement ?

5.Quelles sont les fonctions et les caractéristiques du report ?

6.L'implémentation sous-jacente de Go slice ? Aller au mécanisme d'expansion des tranches ?

7. Les tranches sont-elles les mêmes avant et après expansion ?

8.Pourquoi slice n’est-il pas thread-safe ?


1. La différence entre marque et nouveau

Différences : 1. Make ne peut être utilisé que pour allouer et initialiser des données de types slice, map et chan, tandis que new peut allouer n'importe quel type de données.

2. La nouvelle allocation renvoie un pointeur, qui est le type "*Type" tandis que make renvoie une référence, qui est le Type.

3. L'espace alloué par new sera effacé ; une fois que make aura alloué l'espace, il sera initialisé.

2. Comment utiliser le transfert de valeur et le transfert d'adresse (transfert de référence) en langage Go ? Quelle est la différence?

Passer par valeur copiera uniquement la valeur du paramètre et la mettra dans la fonction correspondante. Les adresses des deux variables sont différentes et ne peuvent pas se modifier.

Le passage d'adresse (passage par référence) transmettra la variable elle-même dans la fonction correspondante, et le contenu de la valeur de la variable peut être modifié dans la fonction.

3. Quelle est la différence entre les tableaux et les tranches en langage Go ? Quelle est la différence entre les tableaux et les tranches lors du passage en langage Go ? Comment le langage Go implémente-t-il l’expansion des tranches ?

Tableau:

Longueur fixe du tableau La longueur du tableau fait partie du type de tableau, donc [3]int et [4]int sont deux types de tableau différents. Le tableau doit spécifier la taille. Si elle n'est pas spécifiée, la taille sera automatiquement calculée en fonction de la taille. paire d'initialisation. Le tableau ne peut pas être modifié est passé par valeur.

tranche:

La longueur d'une tranche peut être modifiée. Une tranche est une structure de données légère. Les trois attributs, pointeur, longueur et capacité, n'ont pas besoin de spécifier la taille. Les tranches sont passées par adresse (passées par référence) et peuvent être initialisées via un tableau ou via la fonction intégrée make(). Lors de l'initialisation, len=cap est ensuite développé.

Les tableaux sont des types valeur et les tranches sont des types référence ;

La longueur des tableaux est fixe, mais pas les tranches (les tranches sont des tableaux dynamiques)

La couche sous-jacente de la tranche est un tableau

Le mécanisme d'expansion de tranche du langage Go est très intelligent. Il réalise l'expansion en réattribuant le tableau sous-jacent et en migrant les données vers un nouveau tableau. Plus précisément, lorsqu'une tranche doit être développée, le langage Go crée un nouveau tableau sous-jacent et copie les données du tableau d'origine dans le nouveau tableau. Ensuite, le pointeur de tranche pointe vers le nouveau tableau, la longueur est mise à jour à la longueur d'origine plus la longueur étendue, et la capacité est mise à jour à la longueur du nouveau tableau.

Pour réaliser une expansion de tranche :

aller1.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. }

Avant d'allouer de l'espace mémoire, vous devez déterminer la nouvelle capacité de la tranche au moment de l'exécution, vous pouvez sélectionner différentes stratégies d'expansion en fonction de la capacité actuelle de la tranche :

Si la capacité attendue est supérieure à deux fois la capacité actuelle, la capacité attendue sera utilisée ; si la longueur de la tranche actuelle est inférieure à 1024, la capacité sera doublée si la longueur de la tranche actuelle est supérieure ou égale à ; 1024, la capacité sera augmentée de 25 % à chaque fois jusqu'à ce que la nouvelle capacité soit supérieure à la capacité prévue ;

aller1.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 différence avec la version précédente réside principalement dans le seuil d'expansion et cette ligne de code :newcap += (newcap + 3*threshold) / 4

Avant d'allouer de l'espace mémoire, vous devez déterminer la nouvelle capacité de la tranche au moment de l'exécution, vous pouvez sélectionner différentes stratégies d'expansion en fonction de la capacité actuelle de la tranche :

  • Si la capacité attendue est supérieure à deux fois la capacité actuelle, la capacité attendue sera utilisée ;

  • Si la longueur de la tranche actuelle est inférieure au seuil (256 par défaut), la capacité sera doublée ;

  • Si la longueur de la tranche actuelle est supérieure ou égale au seuil (par défaut 256), la capacité sera augmentée de 25% à chaque fois. newcap + 3*threshold, jusqu'à ce que la nouvelle capacité soit supérieure à la capacité attendue ;

L'expansion des tranches est divisée en deux étapes, avant et après go1.18 :

1. Avant de partir1.18 :

  • Si la capacité attendue est supérieure à deux fois la capacité actuelle, la capacité attendue sera utilisée ;

  • Si la longueur de la tranche actuelle est inférieure à 1024, la capacité sera doublée ;

  • Si la longueur de la tranche actuelle est supérieure à 1024, la capacité sera augmentée de 25 % à chaque fois jusqu'à ce que la nouvelle capacité soit supérieure à la capacité attendue ;

2. Après go1.18 :

  • Si la capacité attendue est supérieure à deux fois la capacité actuelle, la capacité attendue sera utilisée ;

  • Si la longueur de la tranche actuelle est inférieure au seuil (256 par défaut), la capacité sera doublée ;

  • Si la longueur de la tranche actuelle est supérieure ou égale au seuil (par défaut 256), la capacité sera augmentée de 25% à chaque fois. newcap + 3*threshold, jusqu'à ce que la nouvelle capacité soit supérieure à la capacité attendue ;

4. Qu’est-ce que Go Convey ? A quoi sert-il généralement ?

go carry est un framework de tests unitaires qui prend en charge Golang

go carry peut surveiller automatiquement les modifications de fichiers et démarrer des tests, et peut afficher les résultats des tests sur l'interface Web en temps réel

go transmettre fournit des assertions riches pour simplifier la rédaction des cas de test

5.Quelles sont les fonctions et les caractéristiques du report ?

La fonction du report est :

Il vous suffit d'ajouter le mot-clé defer avant d'appeler une fonction ou une méthode normale pour compléter la syntaxe requise pour defer. Lorsque l'instruction defer est exécutée, la fonction suivant le defer sera différée. La fonction après le defer ne sera pas exécutée tant que la fonction contenant l'instruction defer n'est pas exécutée, que la fonction contenant l'instruction defer se termine normalement par return ou se termine anormalement en raison d'une panique. Vous pouvez exécuter plusieurs instructions defer dans une fonction, dans l'ordre inverse de leur déclaration.

Scénarios courants de report :

L'instruction defer est souvent utilisée pour gérer des opérations couplées, telles que l'ouverture, la fermeture, la connexion, la déconnexion, le verrouillage et le déverrouillage.

Grâce au mécanisme de report, quelle que soit la complexité de la logique de la fonction, il est possible de garantir que les ressources seront libérées dans n'importe quel chemin d'exécution.

 

6.L'implémentation sous-jacente de Go slice ? Aller au mécanisme d'expansion des tranches ?

Le découpage est implémenté sur la base de tableaux. Sa couche sous-jacente est un tableau. Elle est elle-même très petite et peut être comprise comme une abstraction du tableau sous-jacent. Parce qu'il est implémenté sur la base de tableaux, sa mémoire sous-jacente est allouée en continu, ce qui est très efficace. Les données peuvent également être obtenues via des index, des itérations et l'optimisation du garbage collection. Les tranches elles-mêmes ne sont pas des tableaux dynamiques ni des pointeurs de tableau. La structure de données implémentée en interne fait référence au tableau sous-jacent via un pointeur, et les attributs pertinents sont définis pour limiter les opérations de lecture et d'écriture de données à une zone spécifiée. La tranche elle-même est un objet en lecture seule et son mécanisme de fonctionnement est similaire à une encapsulation d'un pointeur de tableau.

L'objet slice est très petit car il s'agit d'une structure de données avec seulement 3 champs :

pointeur vers le tableau sous-jacent

longueur de tranche

capacité de tranche

La stratégie de découpage en expansion dans Go (1.17) est la suivante :

Jugez d'abord, si la capacité nouvellement appliquée est supérieure à 2 fois l'ancienne capacité, la capacité finale sera la capacité nouvellement appliquée.

Sinon, si la longueur de l'ancienne tranche est inférieure à 1024, la capacité finale sera le double de l'ancienne capacité.

Sinon, si l'ancienne longueur de tranche est supérieure ou égale à 1024, la capacité finale sera cycliquement augmentée de 1/4 par rapport à l'ancienne capacité jusqu'à ce que la capacité finale soit supérieure ou égale à la nouvelle capacité demandée.

Si la valeur finale de calcul de la capacité dépasse, la capacité finale est la capacité nouvellement demandée.

7. Les tranches sont-elles les mêmes avant et après expansion ?

Première situation :

Le tableau d'origine a toujours une capacité qui peut être étendue (la capacité réelle n'a pas été remplie. Dans ce cas, le tableau étendu pointe toujours vers le tableau d'origine et l'opération sur une tranche peut affecter plusieurs pointeurs pointant vers la même adresse de). la tranche.

Deuxième situation :

Il s'avère que la capacité du tableau a atteint la valeur maximale. Si vous souhaitez augmenter la capacité, Go ouvrira d'abord une zone mémoire par défaut, copiera la valeur d'origine, puis effectuera l'opération append(). Cette situation n'affecte pas du tout la baie d'origine. Pour copier une Slice, il est préférable d'utiliser la fonction Copier.

8.Pourquoi slice n’est-il pas 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. }