Esta pagina se ve mejor con JavaScript habilitado

Go pointers

 ·  🎃 kr0m

Un puntero no es mas que una variable que no contiene un valor en si misma, si no una dirección de memoria donde se encuentra la variable final.
Utilizar punteros nos permitirá acceder a un conjunto de datos de forma centralizada y sin tener que hacer una copia de estos, si la variable en cuestión es de un gran tamaño supondrá un ahorro considerable de memoria RAM.

Imaginemos que tenemos una variable y un puntero que apunta a dicha variable:

a := 3.1415
ptr = &a

Lo que genera la siguiente disposición de datos en la memoria RAM:

RAM Puntero

Además de lo anterior, debemos tener en cuenta que en Go una función puede recibir el valor de una variable de dos maneras distintas:

  • Por Valor: Se le pasa una copia del valor de la variable.
  • Por Referencia: Se le pasa la dirección de memoria(puntero) donde está la variable, haciendo de esta una variable “compartida”.

Este artículo se divide en varias secciones para una comprensión mas sencilla:


Si eres nuevo en el mundo de Go te recomiendo los siguientes artículos anteriores:


Operaciones sobre punteros:

En la siguiente tabla se indican los operaciones que se pueden realizar sobre punteros:

Acción Operador Ejemplo
Obtener dirección de memoria de una variable. & memoryAddress := &var
Crear un puntero. * Regular mode: var ptr *int = &i
Sugar syntax: ptr := &i
Modificar el valor de la variable utilizando un puntero. * *ptr = 2

Punteros básicos:

Podemos ver como resulta equivalente trabajar con una variable, así como trabajar con un puntero hacia esa variable:

package main

import "fmt"

func main() {
    i := 1
    fmt.Println("i original value:", i)

    // Get pointer
    iptr := &i
    // Change final variable value
    *iptr = 2
    fmt.Println("i modified by pointer:", i)

    fmt.Println("i readed via pointer:", *iptr)

    fmt.Println("Memory address i variable:", &i)
}

Al ejecutar el código obtenemos la siguiente salida:

i original value: 1
i modified by pointer: 2
i readed via pointer: 2
Memory address i variable: 0x86c09e010

Punteros en funciones:

Si una función va a recibir una variable de entrada que sea un puntero, la definiremos del siguiente modo:

func zeroptr(iptr *int) {

En este sencillo ejemplo podemos ver dos funciones, zeroval que recibe el valor de la variable i por copia y la función zeroptr que lo recibe por referencia:

package main

import "fmt"

// Copy value function
func zeroval(ival int) {
    ival = 0
}

// Pointer function
func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("i original value:", i)

    // Pass variable value copy to zeroval() function
    zeroval(i)
    fmt.Println("i zeroval value:", i)

    // Get pointer
    iptr := &i
    // Pass pointer to zeroptr() function
    zeroptr(iptr)
    fmt.Println("i zeroptr value:", i)

    fmt.Println("Memory address i variable:", &i)
}

Al ejecutar el código obtenemos la siguiente salida:

i original value: 1
i zeroval value: 1
i zeroptr value: 0
Memory address i variable: 0x86c09e010

Podemos ver que el valor de la variable i de la función principal solo se ha visto afectada cuando hemos pasado el puntero, ya que de este modo la hemos convertido en una variable “compartida” entre la función principal y zeroptr.

Mediante copia de valor de variables a las funciones tendríamos que sobreescribir el valor de i en la función main:

i = zeroval(i)

Y cuando llamamos a zeroval() retornar un valor:

func zeroval(ival int) int {
    ival = 0
    return ival
}

Obteniendo el mismo resultado que utilizando punteros, con la única diferencia de que estaríamos utilizando un poco mas de RAM, el código final quedaría del siguiente modo:

package main

import "fmt"

// Variable value continues being passed by value
// Yet now it returns an int value
func zeroval(ival int) int {
    ival = 0
    return ival
}

// Pointer function
func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("i original value:", i)

    // Pass variable value copy to zeroval() function
    // Overwrite i value with zeroval() return value
    i = zeroval(i)
    fmt.Println("i zeroval value:", i)

    // Get pointer
    iptr := &i
    // Pass pointer to zeroptr() function
    zeroptr(iptr)
    fmt.Println("i zeroptr value:", i)

    fmt.Println("Memory address i variable:", &i)
}

Al ejecutar el código obtenemos la siguiente salida:

i original value: 1
i zeroval value: 0
i zeroptr value: 0
Memory address i variable: 0x86c09e010

Punteros a arrays/slices:

Cuando modificamos los valores de un array/slice mediante punteros, no es preciso hacerlo utilizando * como hemos visto hasta ahora.

Esto puede hacerse de dos formas distintas:

- (*pointer)[N]
- pointer[N]

Con un ejemplo se verá mas claro:

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {
    (*arr)[0] = 90
}

func modify2(arr *[3]int) {
    arr[0] = 80
}

func main() {  
    a := [3]int{89, 90, 91}
    fmt.Println("array value:", a)

    aPtr := &a

    modify(aPtr)
    fmt.Println("array value:", a)

    modify2(aPtr)
    fmt.Println("array value:", a)
}

Al ejecutar el código obtendremos la siguiente salida:

array value: [89 90 91]
array value: [90 90 91]
array value: [80 90 91]

Recordemos que los slices son directamente punteros, por lo tanto cuando pasamos un slice a una función estamos pasando el puntero:

package main

import (
    "fmt"
)

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := []int{89, 90, 91}
    fmt.Println("slice value:", a)
    modify(a)
    fmt.Println("slice value:", a)
}

Al ejecutar el código obtendremos la siguiente salida:

slice value: [89 90 91]
slice value: [90 90 91]

La forma idiomática de modificar arrays mediante punteros no es pasando el puntero, si no recibiendo el array en la función pero guardándolo en un slice, en este ejemplo se puede ver fácilmente.

package main

import (
    "fmt"
)

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    fmt.Println("array value:", a)
    modify(a[:])
    fmt.Println("array value:", a)
}

Al ejecutar el código obtendremos la siguiente salida:

array value: [89 90 91]
array value: [90 90 91]

Punteros a estructuras:

Cuando modificamos los valores de una estructura mediante punteros ocurre lo mismo que con los arrays, no es necesario utilizar *.

Esto puede hacerse de dos formas distintas:

- (*pointer).myField
- pointer.myField
package main

import "fmt"

type employee struct {
    name   string
    salary float64
}

// Set salary using (*pointer)
func setSalary1 (employeeToModify *employee, salary float64) {
    (*employeeToModify).salary = salary
}

// Set salary using pointer directly
func setSalary2 (employeeToModify *employee, salary float64) {
    employeeToModify.salary = salary
}

func main() {
    employeeOne := employee{
        name:   "John",
        salary: 1000,
    }
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Regular structure variable access
    employeeOne.salary = 2000
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Get pointer
    employeeOnePtr := &employeeOne

    // Access structure variable value using (*pointer)
    setSalary1(employeeOnePtr, 3000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Access structure variable value using pointer directly
    setSalary2(employeeOnePtr, 4000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)
}

Al ejecutar el código obtendremos la siguiente salida:

employeeOne Salary: 1000
employeeOne Salary: 2000
employeeOne Salary: 3000
employeeOne Salary: 4000

Normalmente cuando se trata de punteros a estructuras se accede a sus campos sin utilizar *() ya que resulta mas cómodo, sin embargo si generamos un puntero también podremos acceder a los métodos de la variable final mediante este.

package main

import "fmt"

type employee struct {
    name   string
    salary float64
}

// Show salary method
func (s employee) showSalary() float64 {
    return s.salary
}

func main() {
    employeeOne := employee{
        name:   "John",
        salary: 1000,
    }
    fmt.Println("employeeOne Salary structure method:", employeeOne.showSalary())

	// Get pointer
    employeeOnePtr := &employeeOne

	// Access structure variable value calling ptr method
    fmt.Println("employeeOne Salary pointer method:", employeeOnePtr.showSalary())
}

Al ejecutar el código obtendremos la siguiente salida:

employeeOne Salary structure method: 1000
employeeOne Salary pointer method: 1000

Punteros en los receiver parameters de métodos:

Cuando utilizamos métodos asociados a tipos, ocurre exactamente lo mismo, además debemos recalcar que incluso cuando los métodos requieran un receiver parameter de tipo puntero, no es preciso crear un puntero desde el que llamar a dichos métodos.

En el siguiente ejemplo se altera el valor de los campos de la estructura mediante métodos asociados al tipo employee, donde dichos métodos requieren de un puntero, pero como podemos ver, es igualmente válido llamar a los métodos desde el objeto original como desde el puntero de este:

employeeOne.setSalary1(3000)
employeeOnePtr.setSalary1(5000)

El código completo quedaría del siguiente modo:

package main

import "fmt"

type employee struct {
    name   string
    salary float64
}

// Set salary using (*pointer)
func (s *employee) setSalary1 (salary float64) {
    (*s).salary = salary
}

// Set salary using pointer directly
func (s *employee) setSalary2 (salary float64) {
    s.salary = salary
}

func main() {
    employeeOne := employee{
        name:   "John",
        salary: 1000,
    }
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Regular structure variable access
    employeeOne.salary = 2000
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Access structure variable value using (*pointer)
    employeeOne.setSalary1(3000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Access structure variable value using pointer directly
    employeeOne.setSalary2(4000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Get pointer
    employeeOnePtr := &employeeOne

    // Access structure variable value using (*pointer), calling ptr method
    employeeOnePtr.setSalary1(5000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)

    // Access structure variable value using pointer directly, calling ptr method
    employeeOnePtr.setSalary2(6000)
    fmt.Println("employeeOne Salary:", employeeOne.salary)
}

Al ejecutar el código obtendremos la siguiente salida:

employeeOne Salary: 1000
employeeOne Salary: 2000
employeeOne Salary: 3000
employeeOne Salary: 4000
employeeOne Salary: 5000
employeeOne Salary: 6000

Aritmética de punteros:

Go no soporta aritmética de punteros.

Safety. Without pointer arithmetic it’s possible to create a language that can never derive an illegal address that succeeds incorrectly.
Compiler and hardware technology have advanced to the point where a loop using array indices can be as efficient as a loop using pointer arithmetic.
Also, the lack of pointer arithmetic can simplify the implementation of the garbage collector.

Consideraciones al trabajar con punteros:

Por norma general es mas seguro trabajar mediante copia de valores aunque estemos consumiendo algo mas de RAM, es mas dificil provocar problemas si cada función tiene su propia copia, sobretodo si nuestro software es concurrente.

Solo deberíamos utilizar punteros si nuestras estructuras son lo suficientemente grandes como para suponer un ahorro considerable de RAM. En los casos en los que las variables son muy grandes recomiendan pasar las estructuras mediante punteros incluso cuando no se vaya a modificar el contenido de dicha estructura y solo se vaya a leer de ella.

Además hay un pequeño inconveniente en cuanto a la impredecibilidad del consumo de RAM si utilizamos punteros, ya que cuando se llama a una función:

  • En el stack se crean las variables de esa función en concreto.
  • Si en dicha función retornamos un puntero, la variable a la que apunta residirá en la región de memoria asignada a esa función, lo que se llama frame.
  • Al volver de la función a main, el frame es reutilizado para cualquier otro uso, quedando con un puntero apuntando a una región con datos inválidos.
  • Para resolver esto, al retornar de la función Go mueve la variable al heap de forma automática.
  • El inconveniente de esto es que el garbage collector va a tener que comprobar cada X tiempo que no haya referencias a dicha variable para liberar la RAM del heap en cuanto sea posible.

Podemos ver un ejemplo en el siguiente código:

package main

import "fmt"

func getIntPtr() *int {
    i := 1
    iptr := &i
    return iptr
}

func main() {
    iptr := getIntPtr()
    fmt.Println("iptr value:", *iptr)
}

La variable i de la función getIntPtr() primero se creó en el stack en el frame de dicha función, al retornar Go automáticamente detectó que hay un puntero apuntando a dicha variable y la migró al heap.

Ejecutando el código podemos observar que accediendo al valor de i mediante el puntero, este sigue siendo correcto, no se ha corrompido de ninguna manera:

iptr value: 1

La liberación de la RAM no es inmediata como si que ocurre cuando utilizamos valores por copia. Con punteros se liberará cuando el garbage collector detecte que es posible hacerlo.

Recomendo encarecidamente ver este video donde se explica realmente bien el funcionamiento de punteros en Go.

Si te ha gustado el artículo puedes invitarme a un RedBull aquí