Указатели

Указатели — это переменные, которые указывают на адрес других переменных. Они объявляются почти как обычные переменные, за исключение, что перед типом данных ставится символ звездочки *. Например, определение указателя на объект типа int:

var ptr *int

Данному указателю можно присвоить адрес переменной типа int. Для получение адреса применяется операция &, после которой указывается имя переменной (&a):

package main
import "fmt"

func main() {
    var a int = 1       // определяем переменную
    var ptr *int          // определяем указатель

    ptr = &a              // указатель получает адрес переменной
    fmt.Println(ptr)      // значение самого указателя - адрес переменной a
}

В данном примере указатель ptr хранит адрес переменной a. Что важно, переменная a имеет тип int, и указатель ptr указывает именно на объект типа int. То есть должно быть соответствие по типу. И если мы попробуем вывести адрес переменной на консоль, то увидим, что он представляет шестнадцатеричное значение:

0xc0000a6058

В каждом случае адрес может отличаться, но в текущем примере, машинный адрес переменной a - 0xc0000a6058. Таким образом, в памяти комьютера есть адрес 0xc0000a6058 по которому распологается переменная a.

Оператор разименования

По адресу, который хранит указатель, мы получить значение переменной a. Для этого применяется операция * (операция разыменования). Результатом этой операции является значение переменной, на которую назначен указатель. Если мы применим эту операцию, то получим значение переменной a:

package main
import "fmt"

func main() {
    var a int = 1
    var ptr *int = &a

    fmt.Println("Адрес переменной а:", ptr)     // Адрес переменной а: 0xc000018088
    fmt.Println("Значение переменной а:", *ptr) // Значение переменной а: 1

Более того, через указатель с помощью оператора разименования мы можем изменить значение переменной, на который указывает наш ptr:

package main
import "fmt"

func main() {
    var a int = 1
    var ptr *int = &a

    *ptr = 5
    fmt.Println("Значение переменной а:", a)    // Значение переменной а: 5
}

Также при объявлении указетеля, можно использовать сокразенную форму:

package main
import "fmt"

func main() {
    var b int = 2
    ptr := &b

    fmt.Println("Адрес b:", ptr)        // Адрес b: 0xc000018088
    fmt.Println("Значение b:", *ptr)    // Значение b: 2
}

Пустые указатели

В случае, есил указателю не присвоить адрес какой-либо переменной (или любого другого объекта), то он будет иметь пустое значение (nil). При попытке получить значение такого указателя мы получим ошибку:

package main
import "fmt"

func main() {
    var ptr *string

    fmt.Println("Значение:", *ptr)  // runtime error: invalid memory address or nil pointer dereference
}

Чтобы избежать этой ошибки в Go часто используют следующую коснтрукцию, которая проверяет указатель на наличие адреса:

package main
import "fmt"

func main() {
    var ptr *string

    if ptr != nil {
        fmt.Println("Значение:", *ptr)
    } else {
        fmt.Println("Указатель ptr имеет nil значение!")
    }
}

Функция new

Встроенная функция new выделяет память (создает неименованную переменную и возваращет указатель на ее значение). В эту функцию передается тип, объект которого надо создать. Функция возвращает указатель на созданный объект:

package main
import "fmt"

func main() {
    ptr := new(int)

    fmt.Println("Адрес:", ptr)  // Адрес: 0xc0000a6058
    fmt.Println("Значение до присвоения:", *ptr)    // Значение до присвоения: 0

    *ptr = 10
    fmt.Println("Значение после присвоения:", *ptr) // Значение после присвоения: 10
}

В этом примере указатель ptr будет иметь тип *int, поскольку он указывает на объект типа int. Созданный объект ptr имеет значение по умлочанию (для int это 0).

По сути, объект, созданный с помощью функции new(), ничем не отличается от обычной переменной. Единственное что, чтобы обратиться к этому объекту, наприме, получить или изменить его адрес, необходимо использовать указатель.