Указатель это переменная в которой указан адрес в памяти. В языке С (и в Go) параметры функции передаются по значению и если мы передадим указатель, то внутри функции получим новый указатель на тот же адрес. Из-за этого возникает проблема, когда мы создаем нулевой указатель, передаем его в функцию, а внутри функции этому нулевому указателю присвается адрес. Т.к. внутри функции у нас другой указатель, то переданный в функцию указатель так и остается нулевым после выхода из функции. Для решения данной проблемы используется двойные указатели (указатель на указатель, передача адреса указателя). Рассмотрим подробнее на примерах.
Создаем файл main.go :
package main
/*
int SetNewValueOne(void* p)
{
printf("p=%p &p=%p \n", p, &p);
if(!p)
{
int i=5;
p= &i;
printf("p=%p &p=%p *p=%p \n", p, &p, *(int *)p);
return 1;
}
return 0;
}
int SetNewValue(void** p)
{
printf("p=%p &p=%p *p=%d \n", p, &p, *p);
if(!*p)
{
int i=5;
*p= &i;
printf("p=%p &p=%p *p=%p \n", p, &p, *p);
printf("**p=%d \n", *(int*)(*p));
return 1;
}
return 0;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func callOne() {
println("--- callOne ---")
println("------------------------------------------------------------")
var ptr *int32
fmt.Printf("ptr=%p (%d) &ptr=%p \n", ptr, ptr, &ptr)
C.SetNewValueOne((unsafe.Pointer)(ptr))
fmt.Printf("ptr=%p (%d) &ptr=%p \n", ptr, ptr, &ptr)
if ptr != nil {
fmt.Printf("*ptr=%d *ptr(bits)=%b \n", *ptr, *ptr)
}
println("------------------------------------------------------------")
}
func callDouble() {
println("--- callDouble ---")
println("------------------------------------------------------------")
var ptr *int32
fmt.Printf("ptr=%p (%d) &ptr=%p \n", ptr, ptr, &ptr)
uptr := unsafe.Pointer(&ptr)
fmt.Printf("uptr=%p (%d) &uptr=%p \n", uptr, uptr, &uptr)
C.SetNewValue((*unsafe.Pointer)(uptr))
fmt.Printf("ptr=%p (%d) &ptr=%p \n", ptr, ptr, &ptr)
fmt.Printf("*ptr=%d *ptr(bits)=%b \n", *ptr, *ptr)
println("------------------------------------------------------------")
}
func main() {
callOne()
callDouble()
}
В первом случае передается обычный указатель. Вывод будет такой:
--- callOne ---
------------------------------------------------------------
ptr=0x0 (0) &ptr=0xc00004e020
p=0000000000000000 &p=00000035337FF838
p=00000035337FF834 &p=00000035337FF838 *p=0000000000000005
ptr=0x0 (0) &ptr=0xc00004e020
------------------------------------------------------------
Т.к. параметром С функции у нас является void*, то в Go мы должны привести наш указатель к unsafe.Pointer. Видно, что в Go мы создали один указатель, а внутри С функции уже другой (копирование параметра по значению) и после выхода из функции С у нас так и остался нулевой указатель.
Во втором случае передаем двойной указатель. Вывод будет таким:
--- callDouble ---
------------------------------------------------------------
ptr=0x0 (0) &ptr=0xc00004e030
uptr=0xc00004e030 (824634040368) &uptr=0xc00004e038
p=000000C00004E030 &p=0000006FE23FFD80 *p=0
p=000000C00004E030 &p=0000006FE23FFD80 *p=0000006FE23FFD6C
**p=5
ptr=0x6fe23ffd6c (480537214316) &ptr=0xc00004e030
*ptr=5 *ptr(bits)=101
------------------------------------------------------------
Мы создаем в Go указатель на на наш нулевой указатель и этот двойной указатель (uptr) передаем в С функцию. Внутри С функции получаем первый нулевой указатель и делаем его указывающим на ячейку памяти со значением int. После выхода из С функции наш Go указатель указывает на число 5.
Есть еще следующий нюанс. В Go тип int 64 бита, а в C 32 бита (в Windows 11). Если в Go сделать тип указателя int, а не int32, то вывод будет такой:
--- callDouble ---
------------------------------------------------------------
ptr=0x0 (0) &ptr=0xc00004e030
uptr=0xc00004e030 (824634040368) &uptr=0xc00004e038
p=000000C00004E030 &p=00000054AA3FFD80 *p=0
p=000000C00004E030 &p=00000054AA3FFD80 *p=00000054AA3FFD6C
**p=5
ptr=0x54aa3ffd6c (363633573228) &ptr=0xc00004e030
*ptr=-6178941403171651579 *ptr(bits)=-101010111000000000000100111011111111111111111111111111111111011
------------------------------------------------------------
компилятор ошибки не выдаст.