Como muchos otros lenguajes, Go tiene punteros. Si eres nuevo en el uso de punteros, pueden parecer un poco misteriosos, pero una vez que te acostumbras a ellos, son bastante sencillos. Son una parte crucial de Go, así que tómate un tiempo para entenderlos de verdad.
Antes de profundizar en los detalles, conviene entender el uso de los punteros. Los punteros son una forma de compartir memoria con otras partes de nuestro programa, lo cual es útil por dos razones principales:
Supongamos que tenemos una variable entera normal a:
var a int
Cuando declaramos una variable, Go tiene que encontrar un lugar en la memoria para almacenar su valor. Esto está en gran medida abstraído de nosotros: cuando necesitamos obtener el valor almacenado en esa parte de la memoria, podemos referirnos a él simplemente por el nombre de la variable.
Por ejemplo, cuando escribimos a + 2, en realidad estamos obteniendo el valor almacenado en la memoria asociada a la variable a y sumándole 2.
De forma similar, cuando necesitamos cambiar el valor en la parte de memoria de a, podemos usar el nombre de la variable para hacer una asignación:
a = 3
La parte de memoria asociada a a ahora almacenará el valor 3.
Aunque las variables nos permiten referirnos a valores en memoria, a veces es útil conocer la dirección de memoria a la que apunta la variable. Los punteros contienen las direcciones de memoria de esos valores. Declaras una variable con un tipo puntero anteponiendo un asterisco al tipo subyacente:
var p *int // 'p' contains the memory address of an integer
Aquí declaramos una variable p de tipo «puntero a int» (*int).
Esto significa que p contendrá la dirección de memoria de un entero.
El valor cero de los punteros es nil porque un puntero nil no contiene ninguna dirección de memoria.
Para encontrar la dirección de memoria del valor de una variable, podemos usar el operador &.
Por ejemplo, si queremos encontrar y almacenar la dirección de memoria de la variable a en el puntero p, podemos hacer lo siguiente:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Cuando tenemos un puntero, puede que queramos conocer el valor almacenado en la dirección de memoria que representa el puntero.
Podemos hacerlo usando el operador *:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
La operación *p obtiene el valor almacenado en la dirección de memoria que contiene p.
Esta operación suele llamarse «desreferenciación».
También podemos usar el operador de desreferencia para asignar un nuevo valor a la dirección de memoria a la que hace referencia el puntero:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
Asignar a *pa cambiará el valor almacenado en la dirección de memoria que contiene pa.
Como pa contiene la dirección de memoria de a, al asignar a *pa estamos cambiando el valor de a.
No obstante, una advertencia: comprueba siempre que un puntero no sea nil antes de desreferenciarlo.
Desreferenciar un puntero nil hará que el programa se bloquee en tiempo de ejecución.
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Hasta ahora solo hemos visto punteros a valores primitivos. También podemos crear punteros para estructuras:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
También podríamos haber creado un nuevo Person y almacenado inmediatamente un puntero a él:
var p *Person
p = &Person{Name: "Peter", Age: 22}
Cuando tenemos un puntero a una estructura, no necesitamos desreferenciar el puntero antes de acceder a uno de los campos:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
Los slices y los mapas son tipos especiales porque ya tienen punteros en su implementación. Esto significa que, la mayoría de las veces, no necesitamos crear punteros para estos tipos para compartir la dirección de memoria de sus valores. Imagina que tenemos una función que incrementa el valor de una clave en un mapa:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Si creamos un mapa y llamamos a esta función, los cambios que la función hizo en el mapa persisten después de que la función termina. Este es un comportamiento similar al que obtenemos si usáramos un puntero, pero fíjate en que en este ejemplo no estamos usando ninguna referencia/desreferencia ni nada de la sintaxis de punteros:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
Lo mismo se aplica cuando cambiamos un elemento existente en un slice.
Sin embargo, las acciones que devuelven un nuevo slice como append son un caso especial y puede que no modifiquen el slice fuera de la función.
Esto se debe a la forma en que los slices funcionan internamente, pero no cubriremos esto en detalle en este ejercicio, ya que es un tema más avanzado.
Si tienes mucha curiosidad, puedes leer más sobre esto en Go Blog: Mechanics of 'append'
Una escuela local cerca de ti tiene una asociación de estudiantes muy activa. La asociación de estudiantes está gestionada por un presidente y, cada 2 años, se celebran elecciones para elegir a un nuevo presidente.
En las elecciones de este año, se decidió que se necesitaba un nuevo sistema digital para contar los votos. La escuela necesita tu ayuda para construir este nuevo sistema.
Una de las primeras cosas que necesita el nuevo sistema de votación es un contador de votos. Este contador es una forma de llevar la cuenta de los votos que tiene un candidato concreto.
Crea una función NewVoteCounter que acepte el número de votos iniciales de un candidato y devuelva un puntero que hace referencia a un int, inicializado con el número de votos iniciales dado.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
¡Ya tienes una forma de crear contadores nuevos! Pero ahora te das cuenta de que el nuevo sistema también necesitará una forma de obtener el número de votos de un contador.
Crea una función VoteCount que tome un contador (*int) como argumento y devuelva el número de votos del contador.
Si el contador es nil, debes suponer que no tiene votos:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
¡Por fin es el momento de contar los votos! Ahora necesitas una forma de incrementar los votos de un contador.
Crea una función IncrementVoteCount que tome un contador (*int) como argumento y un número de votos, e incremente el contador en ese número de votos.
Puedes suponer que el puntero que se pasa nunca será nil.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Con todos los votos ya contados, es el momento de preparar el anuncio de los resultados para toda la escuela. Para ello, te das cuenta de que tener solo contadores de votos es insuficiente. Hace falta una forma de asociar el número de votos con un candidato concreto.
Crea una función NewElectionResult que reciba el nombre de un candidato y su número de votos y
devuelva un nuevo resultado electoral.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
La estructura del resultado electoral ya está creada para ti y se define así:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
¡Es el momento de anunciar al nuevo presidente a la escuela!
El presidente se anunciará en los pequeños paneles digitales de mensajes que tiene la escuela.
El mensaje debe mostrar el nombre del nuevo presidente y los votos que obtuvo, con el siguiente formato: <candidate_name> (<votes>).
Un ejemplo de ese mensaje es: "Peter (51)".
Crea una función DisplayResult que reciba un *ElectionResult como argumento y devuelva un string con el mensaje que se debe mostrar.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Para asegurarse de que los resultados finales eran correctos, se volvieron a contar los votos. En el recuento, se descubrió que el número de votos de algunos candidatos difería en uno.
Crea una función DecrementVotesOfCandidate que reciba los resultados finales y el nombre de un candidato cuyo número de votos debes decrementar.
Los resultados finales se dan en forma de un map[string]int, donde las claves son los nombres de los candidatos y los valores son sus votos totales.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Regístrate en Exercism para aprender y dominar Go con 34 conceptos165 ejercicios y mentoría humana real, todo gratis.