package main
import "fmt"
func appendAndChange(numbers []int) {
newNumbers := append(numbers, 42)
newNumbers[0] = 66
fmt.Println("inside", newNumbers)
}
func main() {
slice := []int{1, 2, 3}
fmt.Println("before", slice)
appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is intact")
fmt.Println("-------")
slice = append(slice, 5)
fmt.Println("before", slice)
appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
}
Output:
before [1 2 3]
inside [66 2 3 42]
after [1 2 3]
original slice is intact
-------
before [1 2 3 5]
inside [66 2 3 5 42]
after [66 2 3 5]
original slice is modified
Entonces, ¿por qué dos llamadas a appendAndChange se comportan de manera diferente??????
Las rebanadas de Go tienen longitud y capacidad. La longitud de una porción es el número de elementos que contiene. La capacidad de una porción es el número de elementos del array subyacente, contando desde el primer elemento de la porción. La longitud y la capacidad de una porción s pueden obtenerse mediante las expresiones len(s) y cap(s).
La clave para averiguar por qué vemos este comportamiento se encuentra en la documentación de append:
El valor resultante de append es una porción que contiene todos los elementos de la porción original más los valores proporcionados. Si la matriz de respaldo de s es demasiado pequeña para contener todos los valores proporcionados, se asignará una matriz mayor. La porción devuelta apuntará a la nueva matriz asignada.
Veamos cada paso.
Primero, slice := []int{1, 2, 3}
Se crea una porción con len=3 y capacity=3. La porción apunta a una matriz subyacente donde se almacenan los datos:
![Graphic #1 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/6bFSxFgVImFRUyHsB94S8K/95cc879dbdb6c0b1c69b35205b5cb321/Screen_Shot_2023-07-11_at_9.32.45_AM.png?w=862&h=588&q=100&fm=png)
Siguiente,
appendAndChange(slice)
nuevosNúmeros := append(números, 42)
nuevosNúmeros[0] = 66
donde numbers = slice.
Estamos intentando añadir 42 a slice, pero slice sólo tiene capacidad para 3, y el array de respaldo es demasiado pequeño para que quepan todos los valores, por lo que se asigna un array mayor, y el slice devuelto apunta al array recién asignado.
![Graphic #2 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/20mFrpHkozMOwGxaUcFpA0/31777e376081684ba1e203fe2c37beb0/Screen_Shot_2023-07-11_at_9.33.34_AM.png?w=686&h=806&q=100&fm=png)
Nota: Para este código, la capacidad de la rebanada devuelta parece duplicarse a 6, pero la estrategia de crecimiento no está documentada.
Ahora tenemos un nuevo array de respaldo que nuevosNúmeros
apunta. Cuando volvamos de la función appendAndChange()
la función nuevosNúmeros
sale del ámbito, y nuestra porción original permanece sin cambios.
Ahora llamamos a
slice = append(slice, 5)
![Graphic #3 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/v3kzkyz3LcXSKPLrp6gVz/017cd2a06d5760e823c5a815ac85569a/Screen_Shot_2023-07-11_at_9.34.40_AM.png?w=680&h=668&q=100&fm=png)
Se crea una nueva matriz de respaldo y la variable slice se actualiza para apuntar a ella. La antigua matriz de respaldo ya no tiene punteros y sale del ámbito. Esta vez, cuando appendAndChange
el método append()
no necesita crear otro nuevo array de respaldo, porque hay capacidad suficiente.
nuevosNúmeros := append(números, 42)
nuevosNúmeros[0] = 66
donde números = slice
![Graphic #4 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/3oldNRnVzkx6YBewwF2FbB/7349f2814c844f6cb2671ddaaf4772de/Screen_Shot_2023-07-11_at_9.35.27_AM.png?w=624&h=804&q=100&fm=png)
Ahora, slice y numbers apuntan al nuevo array de respaldo, pero slice tiene una longitud de 4 y nuevosNúmeros
tiene una longitud de 5. Esto conduce a:
nuevosNúmeros = [66, 2, 3, 5, 42]
slice = [66, 2, 3, 5]
Correcto Ir
Hay que reconocer que solucionar este problema es muy sencillo, y la respuesta está en la propia documentación:
Append devuelve la rebanada actualizada. Por lo tanto, es necesario almacenar el resultado de append, a menudo en la variable que contiene la propia rebanada:
https://pkg.go.dev/builtin#append
Esto significa que podemos simplemente devolver nuevosNúmeros
en appendAndChange
y luego asegurarnos de que establecemos el slice a ese valor cuando lo llamamos, así:
https://go.dev/play/p/Vkm72dXxpnx
package main
import "fmt"
func appendAndChange(numbers []int) []int {
newNumbers := append(numbers, 42)
newNumbers[0] = 66
fmt.Println("inside", newNumbers)
return newNumbers
}
func main() {
slice := []int{1, 2, 3}
fmt.Println("before", slice)
slice = appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
fmt.Println("-------")
slice = append(slice, 5)
fmt.Println("before", slice)
slice = appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
}
Output
before [1 2 3]
inside [66 2 3 42]
after [66 2 3 42]
original slice is modified
-------
before [66 2 3 42 5]
inside [66 2 3 42 5 42]
after [66 2 3 42 5 42]
original slice is modified
This works, and technically is the correct way to use append according to the documentation, but developers should not ha
Esto funciona, y técnicamente es la forma correcta de usar append según la documentación, pero los desarrolladores no deberían tener que leer la documentación de la librería base para buscar casos extremos.
Introduciendo Rust
Como comparación, intentemos replicar este comportamiento en Rust, que tiene más comprobaciones de seguridad de compilación.
#[allow(non_snake_case)]
fn appendAndChange(mut numbers: Vec<i32>) -> () {
numbers.push(24);
numbers[0] = 66;
println!("inside {:?}", numbers);
}
fn main() {
let mut slice = vec![1, 2, 3];
println!("before: {:?}", slice);
appendAndChange(slice);
println!("after: {:?}", slice);
println!("original slice is modified");
println!("-------");
slice.push(5);
println!("before: {:?}", slice);
appendAndChange(slice);
println!("after: {:?}", slice);
println!("original slice is modified");
}
Output: (just the first error...)
error[E0382]: borrow of moved value: `slice`
--> src/main.rs:12:29
|
9 | let mut slice = vec![1, 2, 3];
| --------- move occurs because `slice` has type `Vec<i32>`, which does not implement the `Copy` trait
10 | println!("before: {:?}", slice);
11 | appendAndChange(slice);
| ----- value moved here
12 | println!("after: {:?}", slice);
| ^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function appendAndChange to borrow instead if owning the value isn't necessary
Los vecs de Rust son muy similares a los slices de Go. Sin embargo, en lugar de ser una función, push es un método, que debe tomar una referencia mutable a self, y devuelve (). https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push Esta sutil pero poderosa diferencia por sí sola debería ser suficiente para evitar confusiones sobre cómo usar la biblioteca estándar.
Esto falla porque el valor de slice se mueve a appendAndChange, pero luego se vuelve a referenciar después del movimiento. Aquí hay algunas imágenes (muy simplificadas) para explicar lo que está pasando:
Empezamos con:
![Graphic #5 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/6ZErQs6uDwHWUaE4jc1cQg/32db6c3a069f4268152ca1468a5a15fc/Screen_Shot_2023-07-11_at_9.38.56_AM.png?w=750&h=290&q=100&fm=png)
Luego appendAndChange
llega y TOMA todo el valor. Nuestro mensaje de error nos avisa:
--------- movimiento ocurre porque `slice` tiene el tipo `Vec<i32>`, que no implementa el rasgo `Copy
appendAndChange(slice);
| ----- valor movido aquí
![Graphic #6 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/25F3eDrsX1K7lnQ57GYaPO/c470313315cbbb8607d66d57d39b660e/Screen_Shot_2023-07-11_at_9.39.37_AM.png?w=760&h=468&q=100&fm=png)
Rust no permite que el array tenga 2 punteros, así que mueve los datos del array a números, quitando el puntero de slice.
Entonces, se hacen modificaciones a numbers, así que numbers es ahora [66, 2, 3, 24]
y appendAndChange()
vuelve a la función principal.
Hasta ahora, todo se ha ejecutado correctamente, y si no tuviéramos más líneas de código, el programa compilaría y estaría bien. Pero, queremos tener ese nuevo valor numérico de vuelta en nuestro main
println!("después: {:?}", slice);
Ahora este es nuestro error real:
println!("after: {:?}", slice);
| ^^^^^ valor prestado aquí después de mover
El compilador está diciendo, "Espera, ¿estás intentando imprimir 'slice'? Has movido el valor fuera de 'slice'. No voy a poder hacer eso porque slice no tiene valor ahora" Por suerte para nosotros, también da una sugerencia:
nota: considera cambiar este tipo de parámetro en la función `appendAndChange
` a borrow en su lugar si poseer el valor no es necesario.
Rust está diciendo que estamos intentando hacer algo que no es seguro, y nos lo impide. Intentemos evitarlo. Sugieren tomar prestado el parámetro para appendAndChange
. Así que vamos a pasar una referencia mutable:
#[allow(non_snake_case)]
fn appendAndChange(numbers: &mut Vec<i32>) -> () {
numbers.push(24);
numbers[0] = 66;
println!("inside {:?}", numbers);
}
fn main() {
let mut slice = vec![1, 2, 3];
println!("before: {:?}", slice);
appendAndChange(&mut slice);
println!("after: {:?}", slice);
println!("original slice is modified");
println!("-------");
slice.push(5);
println!("before: {:?}", slice);
appendAndChange(&mut slice);
println!("after: {:?}", slice);
println!("original slice is modified");
}
Output:
before: [1, 2, 3]
inside [66, 2, 3, 24]
after: [66, 2, 3, 24]
original slice is modified
-------
before: [66, 2, 3, 24, 5]
inside [66, 2, 3, 24, 5, 24]
after: [66, 2, 3, 24, 5, 24]
original slice is modified
¡Tada! Así de fácil, ya funciona y no hay casos extraños de los que preocuparse. Así es como funciona. Cuando pasamos una referencia mutable de slice a appendAndChange
se crea un nuevo puntero:
![Graphic #7 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/3cPogtzK96dLfjYQVGZV5F/64166c9087fcb481002a8ec69f43f276/Screen_Shot_2023-07-11_at_9.41.59_AM.png?w=896&h=322&q=100&fm=png)
Ahora, los números pueden hacer cambios a los valores subyacentes en la matriz. Cuando appendAndChange
vuelve a la función main()
numbers sale del ámbito, pero slice permanece como puntero a los nuevos valores.
Conclusión
Fundamentalmente, el problema en Go resultaba de dos punteros con diferentes atributos apuntando al mismo valor:
![Graphic #8 - This Golang Behavior is Unexpected... Or is it?](https://images.ctfassets.net/0vvalmm98slw/7qpErjWO9Cpx2udSPRVGBP/b57f0eaff635dbc772e971bb35680ac1/Screen_Shot_2023-07-24_at_3.37.31_PM.png?w=694&h=884&q=100&fm=png)
El compilador de Rust no permitirá que esto ocurra, ¡evitando muchos errores potenciales en tiempo de compilación!