Conceptual Article

Información sobre punteros en Go

Published on February 7, 2020
Español
Información sobre punteros en Go

Introducción

Al escribir software en Go, escribirá funciones y métodos. Pasará datos a esas funciones como argumentos. A veces, las funciones requieren una copia local de los datos y le conviene que el original se mantenga inalterado. Por ejemplo, si maneja un banco y tiene una función que muestra al usuario los cambios en su estado de cuenta dependiendo del plan de ahorro que elija, no le convendrá cambiar el estado de cuenta real de este antes de que seleccione un plan; solo desea utilizarlo en cálculos. Esto se conoce como paso por valor, porque usted envía el valor de la variable a la función, pero no a la variable en sí.

En otros casos, es posible que desee que la función pueda alterar los datos en la variable original. Por ejemplo, cuando el cliente bancario realiza un depósito en su cuenta, le convendrá que la función de deposito pueda acceder al saldo real, no a una copia. En este caso, no necesita enviar los datos reales a la función; solo debe indicar a esta el lugar en el que se encuentran los datos en la memoria. Un tipo de datos denominado puntero contiene la dirección de memoria de los datos, pero no los datos en sí mismos. La dirección de memoria indica a la función el lugar en el que se pueden encontrar los datos, pero no su valor. Puede pasar el puntero a la función en lugar de los datos y, luego, la función puede alterar la variable original establecida. Esto se conoce como paso por referencia, porque el valor de la variable no se pasa a la función; solo se indica su ubicación.

A lo largo de este artículo, creará y usará punteros para compartir el acceso al espacio de memoria de una variable.

Definir y usar punteros

Al usar un puntero en una variable, hay diferentes elementos sintácticos que debe comprender. El primero es el uso de &. Si establece un signo “&” adelante del nombre de una variable, indica que desea obtener la dirección o un puntero para esa variable. El segundo elemento de sintaxis tiene que ver con el uso del operador de asterisco (*) o eliminación de referencias. Al declarar una variable de puntero, al nombre de la variable le sigue el tipo de la variable a la que el puntero apunta, precedido por *, como se muestra a continuación:

var myPointer *int32 = &someint

Esto crea myPointer como puntero en una variable int32 e inicializa el puntero con la dirección someint. En realidad, el puntero no contiene una variable int32, solo incluye la dirección de una.

Veamos un puntero de una string. Con el siguiente código, se declara tanto un valor como un puntero en una cadena:

main.go
package main

import "fmt"

func main() {
	var creature string = "shark"
	var pointer *string = &creature

	fmt.Println("creature =", creature)
	fmt.Println("pointer =", pointer)
}

Ejecute el programa con el siguiente comando:

  1. go run main.go

Al ejecutar el programa, este imprimirá el valor de la variable y la dirección de la ubicación de esta (la dirección del puntero). La dirección de memoria es un número hexadecimal y no está pensada para que un humano pueda leerla. En la práctica, probablemente nunca mostrará una dirección de memoria para verla. Se la mostramos con fines ilustrativos. Debido a que cada programa se crea en su propio espacio de memoria cuando se ejecuta, el valor del puntero será diferente cada vez que lo ejecute y distinto del resultado que se muestra aquí:

Output
creature = shark pointer = 0xc0000721e0

Asignamos el nombre creature a la primera variable que definimos y la configuramos para que sea igual a una string con el valor shark. Luego, creamos otra variable denominada pointer. Esta vez, fijamos el valor de la variable pointer en la dirección de la variable creature. Para almacenar la dirección de un valor en una variable usamos el símbolo &. Esto significa que la variable pointer almacena la **dirección **de la variable creature, no el valor real.

Esta es la razón por la que, al mostrar el valor de pointer, recibimos el valor 0xc0000721e0, que es la dirección en la que se encuentra almacenada la variable creature en la memoria de la computadora.

Si desea imprimir el valor de la variable a la que se apunta desde la variable pointer, debe eliminar la referencia de esa variable. En el siguiente código se utiliza el operador * para eliminar la referencia de la variable pointer y obtener su valor:

main.go

package main

import "fmt"

func main() {
	var creature string = "shark"
	var pointer *string = &creature

	fmt.Println("creature =", creature)
	fmt.Println("pointer =", pointer)

	fmt.Println("*pointer =", *pointer)
}

Si ejecuta este código, verá el siguiente resultado:

Output
creature = shark pointer = 0xc000010200 *pointer = shark

Con la última línea que agregamos ahora se eliminan las referencias de la variable pointer y se imprime el valor que se almacena en la dirección en cuestión.

Si desea modificar el valor almacenado en la ubicación de la variable pointer, puede usar también el operador de eliminación de referencias:

main.go
package main

import "fmt"

func main() {
	var creature string = "shark"
	var pointer *string = &creature

	fmt.Println("creature =", creature)
	fmt.Println("pointer =", pointer)

	fmt.Println("*pointer =", *pointer)

	*pointer = "jellyfish"
	fmt.Println("*pointer =", *pointer)
}

Ejecute este código para ver el resultado:

Output
creature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish

Establecimos el valor al que hace referencia la variable pointer usando el asterisco (*) delante del nombre de la variable y, luego, proporcionamos el nuevo valor jellyfish. Como puede ver, al imprimir el valor sin referencias ahora, se encuentra fijado en jellyfish.

Posiblemente no lo haya notado, pero de hecho también cambiamos el valor de la variable creature. Esto se debe a que la variable pointer apunta, en realidad, a la dirección de la variable creature. Esto significa que, si cambiamos el valor al que se apunta desde la variable pointer, también cambiamos el valor de la variable creature.

main.go
package main

import "fmt"

func main() {
	var creature string = "shark"
	var pointer *string = &creature

	fmt.Println("creature =", creature)
	fmt.Println("pointer =", pointer)

	fmt.Println("*pointer =", *pointer)

	*pointer = "jellyfish"
	fmt.Println("*pointer =", *pointer)

	fmt.Println("creature =", creature)
}

El resultado tiene el siguiente aspecto:

Output
creature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish

Aunque este código muestra el funcionamiento de un puntero, no representa el método típico con el que usará punteros en Go. Es más común usarlos al definir los argumentos de una función y los valores de retorno o al definir métodos de tipos personalizados. Veamos la forma en que usaría punteros con funciones para compartir el acceso a una variable.

Una vez más, tenga en cuenta que imprimiremos el valor de pointer para indicar que se trata de un puntero. En la práctica, no usaría el valor de un puntero, salvo para hacer referencia al valor subyacente para obtener o actualizar ese valor.

Receptores de punteros de funciones

Al escribir una función, puede definir los argumentos que se transmitirán, ya sea por valor o por referencia. Aplicar el paso por valor implica enviar una copia de ese valor a la función, y cualquier cambio en ese argumento dentro de la función solo tiene efecto sobre esa variable dentro de dicha función, no en el lugar desde el cual se pasó. Sin embargo, si al realizar un paso por referencia pasa un puntero a ese argumento, puede cambiar el valor de la función y el de la variable original que se pasó. Puede obtener más información sobre cómo definir funciones en nuestro artículo Cómo definir e invocar funciones en Go.

Para decidir cuándo pasar un puntero en lugar de cuándo se enviará un valor, debe determinar si desea que el valor se cambie o no. Si no desea que el valor cambie, envíelo como valor. Si desea que la función a la que pasa a su variable pueda cambiarla, debe pasarla como puntero.

Para ver la diferencia, primero, veremos una función que pasa un argumento por value:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func main() {
	var creature Creature = Creature{Species: "shark"}

	fmt.Printf("1) %+v\n", creature)
	changeCreature(creature)
	fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature Creature) {
	creature.Species = "jellyfish"
	fmt.Printf("2) %+v\n", creature)
}

El resultado tiene el siguiente aspecto:

Output
1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark}

Primero, creamos un tipo personalizado llamado Creature. Tiene un campo denominado Species, que es una cadena. En la función main, creamos una instancia de nuestro nuevo tipo llamado creature y establecimos el campo Species en sharks. Luego, imprimimos la variable para mostrar el valor actual almacenado en la variable creature.

A continuación, invocamos changeCreature y pasamos una copia de la variable creature.

Según su definición, la función changeCreature toma un argumento llamado creature y es del tipo Creature que definimos anteriormente. A continuación, cambiamos el valor del campo Species por jellyfish e imprimimos. Observe que, dentro de la función changeCreature, el valor de Species ahora es jellyfish e imprime 2) {Species:jellyfish}. Esto se debe a que podemos cambiar el valor dentro del ámbito de nuestra función.

Sin embargo, cuando la última línea de la función main imprime el valor de creature, el valor de Species sigue siendo sharks. La razón por la que el valor no cambió radica en que pasamos la variable por valor. Esto significa que se creó una copia del valor en la memoria y se pasó a la función changeCreature. Esto nos permite tener una función que puede realizar cambios en cualquier argumento que se haya pasado, según sea necesario, pero no afectará a ninguna de esas variables fuera de la función.

A continuación, cambiaremos la función changeCreature para que tome un argumento por referencia. Podemos hacerlo cambiando el tipo de creature a un puntero usando el operador asterisco (*). En lugar de pasar creature, ahora pasaremos un puntero a creature o a *creature. En el ejemplo anterior, creature es una struct que tiene un valor Species de sharks. *creature es un puntero, no una estructura, por lo que su valor es una ubicación de memoria y eso es lo que pasaamos a changeCreature().

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func main() {
	var creature Creature = Creature{Species: "shark"}

	fmt.Printf("1) %+v\n", creature)
	changeCreature(&creature)
	fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
	creature.Species = "jellyfish"
	fmt.Printf("2) %+v\n", creature)
}

Ejecute este código para ver el siguiente resultado:

Output
1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish}

Observe que, ahora, cuando cambiamos el valor de Species a jellyfish en la función changeCreature, también cambia el valor original definido en la función main. Esto se debe a que pasamos la variable creature por referencia, lo que permite el acceso al valor original y la apliación de modificaciones según sea necesario.

Por lo tanto, si desea que una función pueda cambiar un valor, debe pasarla por referencia. Para aplicar paso por referencia, debe pasar el puntero a la variable, no la variable en sí.

Sin embargo, a veces es posible que no tenga un valor real definido para un puntero. En esos casos, puede presentarse una excepción en el programa. Veamos cómo esto sucede y cómo anticiparse a ese posible problema.

Punteros nulos

Todas las variables de Go tienen un valor de cero. Esto se aplica, incluso, a los punteros. Si declara un puntero para un tipo, pero no asigna valor, el valor de cero será nil. nil es una manera de indicar que “no se inicializó nada” para la variable.

En el siguiente programa, definimos un puntero para un tipo Creature, pero nunca creamos esa instancia real de unCreatureni asignamos su dirección a la variable de puntero creature. El valor será nil y no podemos hacer referencia a ninguno de los campos o métodos definidos en el tipo Creature:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func main() {
	var creature *Creature

	fmt.Printf("1) %+v\n", creature)
	changeCreature(creature)
	fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
	creature.Species = "jellyfish"
	fmt.Printf("2) %+v\n", creature)
}

El resultado tiene el siguiente aspecto:

Output
1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86] goroutine 1 [running]: main.changeCreature(0x0) /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26 main.main() /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98 exit status 2

Cuando ejecutamos el programa, imprimió el valor de la variable creature, que es <nil>. Luego, invocamos la función changeCreature y, cuando esa función intenta establecer el valor del campo Species, se produce una excepción. Esto se debe a que en realidad no se creó ninguna instancia de la variable. Por lo tanto, el programa no tiene dónde almacenar realmente el valor y por ello se produce la excepción.

En Go, cuando se recibe un argumento como puntero se suele comprobar si es nulo o no antes de realizar cualquier operación en él para evitar excepciones en el programa.

Esta técnica se usa con frecuencia para comprobar la presencia de nil:

if someVariable == nil {
	// print an error or return from the method or fuction
}

Efectivamente, debe asegurarse de que no se haya pasado un puntero nil a su función o método. Si lo hace, es probable que solo desee obtener un resultado o un error para indicar que se pasó un argumento no válido a la función o al método. Con el siguiente código se muestra la verificación de nil:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func main() {
	var creature *Creature

	fmt.Printf("1) %+v\n", creature)
	changeCreature(creature)
	fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
	if creature == nil {
		fmt.Println("creature is nil")
		return
	}

	creature.Species = "jellyfish"
	fmt.Printf("2) %+v\n", creature)
}

Añadimos una verificación en changeCreature para ver si el valor del argumento creature es nil. Si lo es, imprimimos “creature is nil” y cerramos la función. De lo contrario, continuamos y cambiamos el valor del campo Species. Si ejecutamos el programa, obtendremos el siguiente resultado:

Output
1) <nil> creature is nil 3) <nil>

Observe que, si bien seguimos teniendo un valor nil para la variable creature, ya no se produce una excepción porque verificamos esa situación.

Por último, si creamos una instancia del tipo Creature y la asignamos a la variable creature, el programa cambiará el valor de la manera prevista:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func main() {
	var creature *Creature
	creature = &Creature{Species: "shark"}

	fmt.Printf("1) %+v\n", creature)
	changeCreature(creature)
	fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
	if creature == nil {
		fmt.Println("creature is nil")
		return
	}

	creature.Species = "jellyfish"
	fmt.Printf("2) %+v\n", creature)
}

Ahora que tenemos una instancia del tipo Creature, el programa se ejecutará y obtendremos el siguiente resultado previsto:

Output
1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish}

Cuando se trabaja con punteros, existe la posibilidad de que se produzcan excepciones en el programa. Para evitar las excepciones, debe comprobar si el valor de un puntero es nil antes de intentar acceder a cualquiera de los campos o métodos definidos en él.

A continuación, veremos el efecto del uso de punteros y valores sobre la definición de métodos en un tipo.

Receptores de punteros de métodos

Un receiver en go es el argumento que se define en la declaración de un método. Observe el siguiente código:

type Creature struct {
	Species string
}

func (c Creature) String() string {
	return c.Species
}

El receptor de este método es c Creature. En él, se indica que la instancia c es de tipo Creature y hará referencia a ese tipo a través de esa variable de instancia.

Así como el comportamiento de las funciones es diferente dependiendo de si se envía un argumento como puntero o valor, los métodos también tienen distintos comportamientos. La gran diferencia radica en que, si define un método con un receptor de valor no puede realizar cambios en la instancia de ese tipo en el que se definió el método.

A veces, deseará que su método pueda actualizar la instancia de la variable que usa. Para permitirlo, debe hacer que el receptor sea un puntero.

Agregaremos un método Reset a nuestro tipo Creature para fijar el campo Species en una cadena vacía:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func (c Creature) Reset() {
	c.Species = ""
}

func main() {
	var creature Creature = Creature{Species: "shark"}

	fmt.Printf("1) %+v\n", creature)
	creature.Reset()
	fmt.Printf("2) %+v\n", creature)
}

Si ejecutamos el programa, obtendremos el siguiente resultado:

Output
1) {Species:shark} 2) {Species:shark}

Observe que, aunque fijamos el valor de Species en una cadena vacía en el método Reset, cuando imprimimos el valor de nuestra variable creature en la función main, el valor continúa fijado en shark. Esto se debe a que definimos el método Reset con un receptor value. Eso significa que el método solo tendrá acceso a una copia de la variable creature.

Si queremos tener la posibilidad de modificar la instancia de la variable creature en los métodos, debemos definirla con un receptor pointer:

main.go
package main

import "fmt"

type Creature struct {
	Species string
}

func (c *Creature) Reset() {
	c.Species = ""
}

func main() {
	var creature Creature = Creature{Species: "shark"}

	fmt.Printf("1) %+v\n", creature)
	creature.Reset()
	fmt.Printf("2) %+v\n", creature)
}

Observe que agregamos un asterisco (*) delante del tipo Creature cuando definimos el método Reset. Esto significa que la instancia de Creature que se pasa al método Reset ahora es un puntero y, por lo tanto, cuando realicemos cambios afectará la instancia original de esas variables.

Output
1) {Species:shark} 2) {Species:}

Ahora, el método Reset cambió el valor del campo Species.

Conclusión

La definición de una función o un método como de paso por valor o por_ referencia_ determinará las partes de su programa que pueden realizar cambios en otras partes. Si controla el momento en que esa variable se puede modificar, podrá escribir software que tenga un mejor rendimiento y sea más predecible. Ahora que incorporó conocimientos sobre los punteros, puede ver también la forma en que se utilizan en interfaces.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors

Default avatar

Technical Editor

Editor at DigitalOcean, former book editor at Pragmatic, O’Reilly, and others. Occasional conference speaker. Highly nerdy.


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.