Qual é uma maneira concisa de criar uma fatia 2D no Go?

103

Estou aprendendo Go passando por Um Tour de Go . Um dos exercícios ali me pede para criar uma fatia 2D de dylinhas e dxcolunas contendo uint8. Minha abordagem atual, que funciona, é esta:

a:= make([][]uint8, dy)       // initialize a slice of dy slices
for i:=0;i<dy;i++ {
    a[i] = make([]uint8, dx)  // initialize a slice of dx unit8 in each of dy slices
}

Acho que iterar por meio de cada fatia para inicializá-lo é muito prolixo. E se a fatia tivesse mais dimensões, o código se tornaria pesado. Existe uma maneira concisa de inicializar fatias 2D (ou n-dimensionais) no Go?

Hazrmard
fonte

Respostas:

148

Não existe uma forma mais concisa, o que você fez é da maneira "certa"; porque as fatias são sempre unidimensionais, mas podem ser compostas para construir objetos de dimensões superiores. Veja esta pergunta para mais detalhes: Go: Como é a representação da memória de um array bidimensional .

Uma coisa que você pode simplificar é usar a for rangeconstrução:

a := make([][]uint8, dy)
for i := range a {
    a[i] = make([]uint8, dx)
}

Observe também que se você inicializar sua fatia com um literal composto , você obterá isso "gratuitamente", por exemplo:

a := [][]uint8{
    {0, 1, 2, 3},
    {4, 5, 6, 7},
}
fmt.Println(a) // Output is [[0 1 2 3] [4 5 6 7]]

Sim, isso tem seus limites, pois aparentemente você tem que enumerar todos os elementos; mas existem alguns truques, nomeadamente você não tem que enumerar todos os valores, apenas aqueles que não são os valores zero do tipo de elemento da fatia. Para obter mais detalhes sobre isso, consulte Itens codificados na inicialização do array golang .

Por exemplo, se você deseja uma fatia em que os primeiros 10 elementos são zeros e, em seguida, segue 1e 2, pode ser criada assim:

b := []uint{10: 1, 2}
fmt.Println(b) // Prints [0 0 0 0 0 0 0 0 0 0 1 2]

Observe também que, se você usar matrizes em vez de fatias , elas podem ser criadas com muita facilidade:

c := [5][5]uint8{}
fmt.Println(c)

O resultado é:

[[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

No caso de arrays, você não precisa iterar sobre o array "externo" e inicializar os arrays "internos", pois os arrays não são descritores, mas valores. Veja a postagem do blog Matrizes, fatias (e strings): A mecânica de 'anexar' para mais detalhes.

Experimente os exemplos no Go Playground .

icza
fonte
Já que usar uma matriz simplifica o código, eu gostaria de fazer isso. Como especificar isso em uma estrutura? Eu entendo cannot use [5][2]string literal (type [5][2]string) as type [][]string in field valuequando tento atribuir o array ao que acho que estou dizendo a Go é um slice.
Eric Lindsey
Descobri sozinho e editei a resposta para adicionar as informações.
Eric Lindsey
1
@EricLindsey Embora sua edição seja boa, ainda vou rejeitá-la porque não quero encorajar o uso de matrizes apenas porque a inicialização é mais fácil. Em Go, os arrays são secundários, e as fatias são o caminho a seguir. Para obter detalhes, consulte Qual é a maneira mais rápida de anexar um array a outro no Go? Os arrays também têm seus lugares. Para obter detalhes, consulte Por que ter arrays no Go?
icza de
justo, mas acredito que a informação ainda tem mérito. O que eu estava tentando explicar com minha edição é que, se você precisa da flexibilidade de diferentes dimensões entre objetos, então as fatias são o caminho a percorrer. Por outro lado, se suas informações forem rigidamente estruturadas e sempre serão as mesmas, os arrays não são apenas mais fáceis de inicializar, mas também mais eficientes. Como posso melhorar a edição?
Eric Lindsey
@EricLindsey Vejo que você fez outra edição que já foi rejeitada por outros. Em sua edição, você disse para usar arrays para ter acesso mais rápido aos elementos. Observe que Go otimiza muitas coisas, e pode não ser o caso, pois as fatias podem ser tão rápidas quanto. Para obter detalhes, consulte Array vs Slice: velocidade de acesso .
icza
12

Existem duas maneiras de usar fatias para criar uma matriz. Vamos dar uma olhada nas diferenças entre eles.

Primeiro método:

matrix := make([][]int, n)
for i := 0; i < n; i++ {
    matrix[i] = make([]int, m)
}

Segundo método:

matrix := make([][]int, n)
rows := make([]int, n*m)
for i := 0; i < n; i++ {
    matrix[i] = rows[i*m : (i+1)*m]
}

Em relação ao primeiro método, fazer makechamadas sucessivas não garante que você acabará com uma matriz contígua, portanto, você pode ter a matriz dividida na memória. Vamos pensar em um exemplo com duas rotinas Go que podem causar isso:

  1. A rotina # 0 é executada make([][]int, n)para obter memória alocada matrix, obtendo um pedaço de memória de 0x000 a 0x07F.
  2. Em seguida, ele inicia o loop e faz a primeira linha make([]int, m), indo de 0x080 a 0x0FF.
  3. Na segunda iteração, ele é interrompido pelo planejador.
  4. O planejador dá ao processador a rotina # 1 e ele começa a funcionar. Este também usa make(para seus próprios propósitos) e vai de 0x100 a 0x17F (próximo à primeira linha da rotina # 0).
  5. Depois de um tempo, ele é interrompido e a rotina # 0 começa a funcionar novamente.
  6. Ele faz o make([]int, m)correspondente à segunda iteração do loop e vai de 0x180 a 0x1FF para a segunda linha. Neste ponto, já temos duas linhas divididas.

Com o segundo método, a rotina faz make([]int, n*m)para obter toda a matriz alocada em um único slice, garantindo a contiguidade. Depois disso, um loop é necessário para atualizar os ponteiros da matriz para os sub-segmentos correspondentes a cada linha.

Você pode brincar com o código mostrado acima no Go Playground para ver a diferença na memória atribuída usando os dois métodos. Observe que usei runtime.Gosched()apenas com o objetivo de ceder o processador e forçar o escalonador a mudar para outra rotina.

Qual usar? Imagine o pior caso com o primeiro método, ou seja, cada linha não é a próxima na memória a outra linha. Então, se seu programa itera através dos elementos da matriz (para lê-los ou gravá-los), provavelmente haverá mais perdas de cache (portanto, latência mais alta) em comparação com o segundo método por causa da pior localidade dos dados. Por outro lado, com o segundo método pode não ser possível obter um único pedaço de memória alocado para a matriz, por causa da fragmentação da memória (pedaços espalhados por toda a memória), embora teoricamente possa haver memória livre suficiente para isso .

Portanto, a menos que haja muita fragmentação de memória e a matriz a ser alocada seja grande o suficiente, você sempre desejaria usar o segundo método para obter vantagem da localidade dos dados.

Marcos Canales Mayo
fonte
2
golang.org/doc/effective_go.html#slices mostra uma maneira inteligente de fazer a técnica de memória contígua aproveitando a sintaxe nativa de fatia (por exemplo, não há necessidade de calcular explicitamente os limites de fatia com expressões como (i + 1) * m)
Magnus
0

Nas respostas anteriores não consideramos a situação em que o comprimento inicial é desconhecido. Para este caso, você pode usar a seguinte lógica para criar a matriz

items := []string{"1.0", "1.0.1", "1.0.2", "1.0.2.1.0"}
mx := make([][]string, 0)
for _, item := range items {
    ind := strings.Count(item, ".")
    for len(mx) < ind+1 {
        mx = append(mx, make([]string, 0))
    }
    mx[ind] = append(mx[ind], item)

}

fmt.Println(mx)

https://play.golang.org/p/pHgggHr4nbB

Máxima
fonte
1
Não tenho certeza se isso está dentro dos limites do OP de "forma concisa", como ele afirmou "Acho que iterar por meio de cada fatia para inicializá-lo é muito prolixo".
Marcos Canales Mayo