Compartilhamento de tecnologia

[Pergunta da entrevista] Golang (Parte 4)

2024-07-12

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

Índice

1.A diferença entre marca e novo

2. Como usar a transferência de valor e a transferência de endereço (transferência de referência) na linguagem Go? Qual é a diferença?

3. Qual é a diferença entre arrays e fatias na linguagem Go? Qual é a diferença entre arrays e fatias ao passar na linguagem Go? Como a linguagem Go implementa a expansão de fatias?

4.O que é Go Convey? Para que geralmente é usado?

5.Quais são as funções e características do adiamento?

6.A implementação subjacente do Go slice? Vai mecanismo de expansão de fatia?

7. As fatias são iguais antes e depois da expansão?

8.Por que o slice não é seguro para threads?


1.A diferença entre marca e novo

Diferenças: 1. Make só pode ser usado para alocar e inicializar dados dos tipos slice, map e chan enquanto new pode alocar qualquer tipo de dados.

2. A nova alocação retorna um ponteiro, que é do tipo "*Type" enquanto make retorna uma referência, que é Type;

3. O espaço alocado por new será limpo após make alocar o espaço, ele será inicializado;

2. Como usar a transferência de valor e a transferência de endereço (transferência de referência) na linguagem Go? Qual é a diferença?

Passar por valor apenas copiará o valor do parâmetro e o colocará na função correspondente. Os endereços das duas variáveis ​​são diferentes e não podem ser modificados.

A passagem de endereço (passagem por referência) passará a própria variável para a função correspondente, e o conteúdo do valor da variável pode ser modificado na função.

3. Qual é a diferença entre arrays e fatias na linguagem Go? Qual é a diferença entre arrays e fatias ao passar na linguagem Go? Como a linguagem Go implementa a expansão de fatias?

Variedade:

Comprimento fixo do array O comprimento do array faz parte do tipo de array, então [3]int e [4]int são dois tipos de array diferentes. O tamanho do array precisa ser especificado. par de inicialização. A matriz não pode ser alterada é passada por valor.

fatiar:

O comprimento de uma fatia pode ser alterado. Uma fatia é uma estrutura de dados leve. Os três atributos, ponteiro, comprimento e capacidade, não precisam especificar o tamanho. As fatias são passadas por endereço (passadas por referência) e podem ser inicializadas por meio de um array ou por meio da função interna make(). Durante a inicialização, len=cap é então expandido.

Matrizes são tipos de valor e fatias são tipos de referência;

O comprimento das matrizes é fixo, mas as fatias não (as fatias são matrizes dinâmicas)

A camada subjacente da fatia é uma matriz

O mecanismo de expansão de fatias da linguagem Go é muito inteligente. Ele consegue a expansão realocando o array subjacente e migrando os dados para um novo array. Especificamente, quando uma fatia precisa ser expandida, a linguagem Go cria um novo array subjacente e copia os dados do array original para o novo array. Em seguida, o ponteiro da fatia aponta para a nova matriz, o comprimento é atualizado para o comprimento original mais o comprimento expandido e a capacidade é atualizada para o comprimento da nova matriz.

Para alcançar a expansão da fatia:

ir1.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 alocar espaço de memória, é necessário determinar a capacidade da nova fatia. No tempo de execução, você pode selecionar diferentes estratégias para expansão com base na capacidade atual da fatia.

Se a capacidade esperada for maior que o dobro da capacidade atual, a capacidade esperada será utilizada; se o comprimento da fatia atual for menor que 1024, a capacidade será duplicada se o comprimento da fatia atual for maior ou igual; 1024, a capacidade será aumentada em 25% a cada vez até que a nova capacidade seja maior que a capacidade esperada;

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

A diferença da versão anterior está principalmente no limite de expansão e nesta linha de código:newcap += (newcap + 3*threshold) / 4

Antes de alocar espaço de memória, é necessário determinar a capacidade da nova fatia. No tempo de execução, você pode selecionar diferentes estratégias para expansão com base na capacidade atual da fatia.

  • Se a capacidade esperada for superior ao dobro da capacidade atual, a capacidade esperada será utilizada;

  • Se o comprimento da fatia atual for menor que o limite (padrão 256), a capacidade será duplicada;

  • Se o comprimento da fatia atual for maior ou igual ao limite (padrão 256), a capacidade será aumentada em 25% a cada vez. newcap + 3*threshold, até que a nova capacidade seja superior à capacidade esperada;

A expansão do slice é dividida em duas etapas, antes e depois do go1.18:

1. Antes de ir1.18:

  • Se a capacidade esperada for superior ao dobro da capacidade atual, a capacidade esperada será utilizada;

  • Se o comprimento da fatia atual for inferior a 1024, a capacidade será duplicada;

  • Se o comprimento da fatia atual for superior a 1024, a capacidade será aumentada em 25% a cada vez até que a nova capacidade seja maior que a capacidade esperada;

2. Após go1.18:

  • Se a capacidade esperada for superior ao dobro da capacidade atual, a capacidade esperada será utilizada;

  • Se o comprimento da fatia atual for menor que o limite (padrão 256), a capacidade será duplicada;

  • Se o comprimento da fatia atual for maior ou igual ao limite (padrão 256), a capacidade será aumentada em 25% a cada vez. newcap + 3*threshold, até que a nova capacidade seja superior à capacidade esperada;

4.O que é Go Convey? Para que geralmente é usado?

go transmit é uma estrutura de teste de unidade que suporta golang

go transmit pode monitorar automaticamente modificações de arquivos e iniciar testes, e pode enviar resultados de testes para a interface da web em tempo real

go transmit fornece afirmações ricas para simplificar a escrita de casos de teste

5.Quais são as funções e características do adiamento?

A função de adiar é:

Você só precisa adicionar a palavra-chave defer antes de chamar uma função ou método normal para completar a sintaxe necessária para defer. Quando a instrução defer for executada, a função após o defer será adiada. A função após o defer não será executada até que a função que contém a instrução defer seja executada, independentemente de a função que contém a instrução defer terminar normalmente por meio de retorno ou terminar de forma anormal devido ao pânico. Você pode executar várias instruções defer em uma função, na ordem inversa de sua declaração.

Cenários comuns para adiar:

A instrução defer é frequentemente usada para lidar com operações emparelhadas, como abrir, fechar, conectar, desconectar, bloquear e liberar bloqueios.

Através do mecanismo de defer, não importa quão complexa seja a lógica da função, pode-se garantir que os recursos serão liberados em qualquer caminho de execução.

 

6.A implementação subjacente do Go slice? Vai mecanismo de expansão de fatia?

O fatiamento é implementado com base em arrays. Sua camada subjacente é um array. Ela própria é muito pequena e pode ser entendida como uma abstração do array subjacente. Por ser implementado com base em arrays, sua memória subjacente é alocada continuamente, o que é muito eficiente. Os dados também podem ser obtidos por meio de índices, podendo ser iterados e otimizados para coleta de lixo. As próprias fatias não são matrizes dinâmicas ou ponteiros de matriz. A estrutura de dados implementada internamente refere-se à matriz subjacente por meio de um ponteiro, e atributos relevantes são definidos para limitar as operações de leitura e gravação de dados em uma área especificada. A fatia em si é um objeto somente leitura e seu mecanismo de funcionamento é semelhante ao encapsulamento de um ponteiro de array.

O objeto slice é muito pequeno porque é uma estrutura de dados com apenas 3 campos:

ponteiro para array subjacente

comprimento da fatia

capacidade de fatia

A estratégia para fatiar a expansão em Go (1.17) é a seguinte:

Em primeiro lugar, se a capacidade recentemente aplicada for superior a 2 vezes a capacidade antiga, a capacidade final será a capacidade recentemente aplicada.

Caso contrário, se o comprimento da fatia antiga for inferior a 1024, a capacidade final será o dobro da capacidade antiga.

Caso contrário, se o comprimento da fatia antiga for maior ou igual a 1024, a capacidade final será ciclicamente aumentada em 1/4 da capacidade antiga até que a capacidade final seja maior ou igual à capacidade recentemente solicitada.

Se o valor do cálculo da capacidade final ultrapassar o limite, a capacidade final será a capacidade recém-solicitada.

7. As fatias são iguais antes e depois da expansão?

Situação um:

O array original ainda tem capacidade que pode ser expandida (a capacidade real não foi preenchida). Nesse caso, o array expandido ainda aponta para o array original. A operação de uma fatia pode afetar vários ponteiros apontando para o mesmo endereço do array. Fatiar.

Situação dois:

Acontece que a capacidade do array atingiu o valor máximo. Se você quiser expandir a capacidade, Go primeiro abrirá uma área de memória por padrão, copiará o valor original e, em seguida, executará a operação append(). Esta situação não afeta em nada o array original. Para copiar um Slice, é melhor usar a função Copiar.

8.Por que o slice não é seguro para threads?

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