Compartir tecnología

[Pregunta de la entrevista] Golang (Parte 4)

2024-07-12

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

Tabla de contenido

1.La diferencia entre marca y nueva

2. ¿Cómo utilizar la transferencia de valor y la transferencia de dirección (transferencia de referencia) en el idioma Go? ¿Cual es la diferencia?

3. ¿Cuál es la diferencia entre matrices y sectores en el lenguaje Go? ¿Cuál es la diferencia entre matrices y sectores al pasar en lenguaje Go? ¿Cómo implementa el lenguaje Go la expansión de sectores?

4. ¿Qué es Go Convey? ¿Para qué se utiliza generalmente?

5. ¿Cuáles son las funciones y características del aplazamiento?

6. ¿La implementación subyacente de Go slice? ¿Ir al mecanismo de expansión de cortes?

7. ¿Las rodajas son iguales antes y después de la expansión?

8. ¿Por qué el segmento no es seguro para subprocesos?


1.La diferencia entre marca y nueva

Diferencias: 1. Make solo se puede usar para asignar e inicializar datos de los tipos slice, map y chan, mientras que new puede asignar cualquier tipo de datos;

2. La nueva asignación devuelve un puntero, que es el tipo "*Tipo", mientras que make devuelve una referencia, que es Tipo.

3. El espacio asignado por new se borrará; después de que make asigne el espacio, se inicializará.

2. ¿Cómo utilizar la transferencia de valor y la transferencia de dirección (transferencia de referencia) en el idioma Go? ¿Cual es la diferencia?

Pasar por valor solo copiará el valor del parámetro y lo colocará en la función correspondiente. Las direcciones de las dos variables son diferentes y no pueden modificarse entre sí.

El paso de dirección (paso por referencia) pasará la variable misma a la función correspondiente, y el contenido del valor de la variable se puede modificar en la función.

3. ¿Cuál es la diferencia entre matrices y sectores en el lenguaje Go? ¿Cuál es la diferencia entre matrices y sectores al pasar en lenguaje Go? ¿Cómo implementa el lenguaje Go la expansión de sectores?

Formación:

Longitud fija de la matriz La longitud de la matriz es parte del tipo de matriz, por lo que [3]int y [4]int son dos tipos de matriz diferentes. Si no se especifica, el tamaño se calculará automáticamente en función del tamaño. par de inicialización. La matriz no se puede cambiar por valor.

rebanada:

La longitud de un segmento se puede cambiar. Un segmento es una estructura de datos liviana. Los tres atributos, puntero, longitud y capacidad, no necesitan especificar el tamaño. Los sectores se pasan por dirección (pasar por referencia) y se pueden inicializar a través de una matriz o mediante la función incorporada make() Durante la inicialización, luego se expande len=cap.

Las matrices son tipos de valores y los sectores son tipos de referencia;

La longitud de las matrices es fija, pero los sectores no (los sectores son matrices dinámicas)

La capa subyacente del segmento es una matriz.

El mecanismo de expansión de sectores del lenguaje Go es muy inteligente. Logra la expansión reasignando la matriz subyacente y migrando datos a una nueva matriz. Específicamente, cuando es necesario expandir un segmento, el lenguaje Go crea una nueva matriz subyacente y copia los datos de la matriz original en la nueva matriz. Luego, el puntero de segmento apunta a la nueva matriz, la longitud se actualiza a la longitud original más la longitud expandida y la capacidad se actualiza a la longitud de la nueva matriz.

Para lograr la expansión del sector:

ir 1.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. }

Antes de asignar espacio de memoria, debe determinar la capacidad del nuevo segmento. En tiempo de ejecución, puede seleccionar diferentes estrategias de expansión según la capacidad actual del segmento:

Si la capacidad esperada es mayor que el doble de la capacidad actual, se utilizará la capacidad esperada; si la longitud del segmento actual es menor que 1024, la capacidad se duplicará si la longitud del segmento actual es mayor o igual; 1024, la capacidad se incrementará en un 25% cada vez hasta que la nueva capacidad sea mayor que la capacidad esperada;

ir 1.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 diferencia con la versión anterior está principalmente en el umbral de expansión y esta línea de código:newcap += (newcap + 3*threshold) / 4

Antes de asignar espacio de memoria, debe determinar la capacidad del nuevo segmento. En tiempo de ejecución, puede seleccionar diferentes estrategias de expansión según la capacidad actual del segmento:

  • Si la capacidad esperada es mayor al doble de la capacidad actual, se utilizará la capacidad esperada;

  • Si la longitud del segmento actual es menor que el umbral (predeterminado 256), la capacidad se duplicará;

  • Si la longitud del segmento actual es mayor o igual que el umbral (predeterminado 256), la capacidad aumentará en un 25% cada vez. newcap + 3*threshold, hasta que la nueva capacidad sea mayor que la capacidad esperada;

La expansión del sector se divide en dos etapas, antes y después de go1.18:

1. Antes de ir 1.18:

  • Si la capacidad esperada es mayor al doble de la capacidad actual, se utilizará la capacidad esperada;

  • Si la longitud del segmento actual es inferior a 1024, la capacidad se duplicará;

  • Si la longitud del segmento actual es mayor que 1024, la capacidad aumentará en un 25% cada vez hasta que la nueva capacidad sea mayor que la capacidad esperada;

2. Después de ir 1.18:

  • Si la capacidad esperada es mayor al doble de la capacidad actual, se utilizará la capacidad esperada;

  • Si la longitud del segmento actual es menor que el umbral (predeterminado 256), la capacidad se duplicará;

  • Si la longitud del segmento actual es mayor o igual que el umbral (predeterminado 256), la capacidad aumentará en un 25% cada vez. newcap + 3*threshold, hasta que la nueva capacidad sea mayor que la capacidad esperada;

4. ¿Qué es Go Convey? ¿Para qué se utiliza generalmente?

go transmit es un marco de prueba unitario que admite golang

go transmit puede monitorear automáticamente las modificaciones de archivos e iniciar pruebas, y puede enviar los resultados de las pruebas a la interfaz web en tiempo real

gotransmitir proporciona afirmaciones ricas para simplificar la redacción de casos de prueba

5. ¿Cuáles son las funciones y características del aplazamiento?

La función de aplazar es:

Solo necesita agregar la palabra clave aplazar antes de llamar a una función o método normal para completar la sintaxis requerida para aplazar. Cuando se ejecuta la declaración de aplazamiento, se aplazará la función que sigue al aplazamiento. La función después del aplazamiento no se ejecutará hasta que se ejecute la función que contiene la declaración aplazar, independientemente de si la función que contiene la declaración aplazar finaliza normalmente mediante retorno o finaliza anormalmente debido al pánico. Puede ejecutar varias declaraciones diferidas en una función, en el orden inverso a su declaración.

Escenarios comunes para aplazar:

La declaración defer se usa a menudo para manejar operaciones emparejadas, como abrir, cerrar, conectar, desconectar, bloquear y liberar cerraduras.

A través del mecanismo de aplazamiento, no importa cuán compleja sea la lógica de la función, se puede garantizar la liberación de recursos en cualquier ruta de ejecución.

 

6. ¿La implementación subyacente de Go slice? ¿Ir al mecanismo de expansión de cortes?

El corte se implementa en base a matrices. Su capa subyacente es una matriz. En sí misma, es muy pequeña y puede entenderse como una abstracción de la matriz subyacente. Debido a que se implementa en base a matrices, su memoria subyacente se asigna continuamente, lo cual es muy eficiente. Los datos también se pueden obtener a través de índices y se pueden iterar y optimizar la recolección de basura. Los sectores en sí no son matrices dinámicas ni punteros de matriz. La estructura de datos implementada internamente se refiere a la matriz subyacente a través de un puntero y los atributos relevantes se configuran para limitar las operaciones de lectura y escritura de datos a un área específica. El segmento en sí es un objeto de solo lectura y su mecanismo de funcionamiento es similar a la encapsulación de un puntero de matriz.

El objeto de corte es muy pequeño porque es una estructura de datos con solo 3 campos:

puntero a la matriz subyacente

longitud del corte

capacidad de corte

La estrategia para dividir la expansión en Go (1.17) es la siguiente:

Primer juez, si la capacidad recién aplicada es mayor que 2 veces la capacidad anterior, la capacidad final será la capacidad recién aplicada.

De lo contrario, si la longitud del segmento anterior es inferior a 1024, la capacidad final será el doble de la capacidad anterior.

De lo contrario, si la longitud del segmento anterior es mayor o igual a 1024, la capacidad final aumentará cíclicamente en 1/4 de la capacidad anterior hasta que la capacidad final sea mayor o igual a la capacidad recién solicitada.

Si el valor del cálculo de la capacidad final se desborda, la capacidad final es la capacidad recién solicitada.

7. ¿Las rodajas son iguales antes y después de la expansión?

Situación uno:

La matriz original todavía tiene capacidad que se puede expandir (la capacidad real no se ha llenado). En este caso, la matriz expandida aún apunta a la matriz original. La operación de un segmento puede afectar varios punteros que apuntan a la misma dirección. Rebanada.

Situación dos:

Resulta que la capacidad de la matriz ha alcanzado el valor máximo. Si desea ampliar la capacidad, Go primero abrirá un área de memoria de forma predeterminada, copiará el valor original y luego realizará la operación append(). Esta situación no afecta en absoluto a la matriz original. Para copiar un sector, es mejor utilizar la función Copiar.

8. ¿Por qué el segmento no es seguro para subprocesos?

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