본문으로 바로가기

C랑 비슷하지만 다르다.

  • C언어에서는 배열이름 자체가 배열의 첫번째 인덱스 요소의 주솟값인데 Go언어는 그런 것이 없습니다. 주솟값은 어떤 변수 앞에 &를 붙이는 것만 기억하면 됩니다.
  • C언어에서는 "*(배열이름+인덱스)"는 "배열이름[인덱스]"와 같은 기능을 했는데 Go언어는 그런 것이 없습니다. 직접 참조를 원하면 포인터 변수 앞에 *를 붙이는 것만 기억하면 됩니다.
  • 함수를 호출할 때는 주솟값 전달을 위해 "함수이름(&변수이름)"을 입력하고 함수에서 매개변수이름을 입력할 때는 값을 직접 찹조하기 위해 *을 매개변수형 앞에 붙입니다. 그리고 함수 안에서 매개변수앞에 모두 *를 붙여야합니다.

 

Go만 쓰신다면 포인터 / 역참조에 대한 개념은 가볍게 다음과 같이 이해할 수 있습니다.

  • 값은 메모리에 저장되며 변수는 일종의 alias이다.
  • 메모리 주소를 값으로 가진 변수를 포인터라고 부른다.
  • 포인터가 가리키는 값을 가져오는 건 역참조라고 한다.
  • 메모리 주소를 직접 대입하거나 포인터 연산을 허용하지 않습니다.

 

기본적인 pointer 사용법

 

&을 통해 메모리값을 출력할 수 있다.

func main() {
	a := 2
	b := a // 값 복사 (primitive type)
	a = 10
	fmt.Println(&a, &b) // &를 붙여 메모리 주소 확인 가능 0xc0000180b8 0xc0000180d0 다른 메모리 주소
}

 

메모리 값을 변수에 할당하면 그게 포인터다. 아래 코드에서 b는 포인터다.

*를 통해 역참조를 할 수 있고 더 나아가 해당 메모리에 특정 값을 다시 넣을 수도 있다.

func main() {
	a := 5
	b := &a // b는 포인터다
	fmt.Println(b)
	fmt.Println(*b) // 역참조
}

 

:= 축약형이 아닌 *자료형 꼴을 이용해 포인터 변수를 만들 수도 있다.

func main() {
	var a *int // 포인터 변수. a의 역참조값은 int여야 함
	b := 3
	a = &b // 주소를 할당해줘야 함
    
	fmt.Println(a) // 주소
	fmt.Println(*a) // 역참조
}

 

C를 사용해오신 분이라면 아래 코드가 더욱 익숙할 것이다. 아래의 경우 *int로 numPtr이라는 포인터를 만들었다. 

func main() {
	var numPtr *int = new(int) // 자료형에 맞는 메모리 공간(int = 4byte)을 할당함
	*numPtr = 10 // 해당 메모리 주소에 값 10 할당
	fmt.Println(numPtr) // 0xc0000120b0
	fmt.Println(*numPtr) // 10
}

 

== 연산자를 통해서 포인터가 같은 메모리 주소를 가리키는지 확인할 수도 있다.

func main() {
	var a int = 10
	var b int = 20

	var p1 *int = &a
	var p2 *int = &b
	var p3 *int = &a

	fmt.Printf("%v\n", p1 == p2) // false
	fmt.Printf("%v", p1 == p3) // true
}

 

 

이제 아래 코드를 이해할 수 있을 것이다. 메모리 주소(&), 역참조(*)만 알면 Go에서 low level의 기본은 완료!

package main

import "fmt"

func main() {
	a := 2
	b := &a            // b는 a의 포인터다
	fmt.Println(&a, b) // 당연히 같은 메모리 주소

	*b = 5         // 해당 메모리의 값을 5로 변경
	fmt.Println(a) // 5 출력
}

 

 

Pointer를 조금 더 써보자 : 스코프 바깥의 변수 접근

 

아래 코드에서 main 함수의 스코프에 있는 프린트에는 0이 찍힙니다. 40이 찍히게 만들고 싶은데, 이를 포인터를 활용해보겠습니다.

func main() {
	x := 0
	foo(x)
	fmt.Println(x) // 0 왜? foo 함수의 스코프 외부이므로
}

func foo(x int) {
	fmt.Println(x)
	x = 40
	fmt.Println(x)
}

 

아래와 같이 짠다면, x의 메모리 주소값을 통해 x를 조작하므로 스코프와 상관없이 40을 띄울 수 있습니다.

func main() {
	x := 0
	foo(&x)
	fmt.Println(x) // 40
}

func foo(x *int) {
	fmt.Println(*x)
	*x = 40
	fmt.Println(*x)
}

 

 

 

Pointer를 언제 써야 하나? : 값복사가 발생하여 메모리를 낭비하고 싶지 않을 때

 

Pointers allow you to share a value stored in some memory location. Use pointers when 

  1. you don’t want to pass around a lot of data (큰 데이터를 복사해서 메모리를 낭비하고 싶지 않을 때)
  2. 메모리 주소에 따라 값을 변경하고 싶은 경우 (앞서 든 예시가 여기에 속합니다)

pointers are good if you have a large chunk of data and you don't want to pass that big chunk of

data around through your program you just pass that address where that data is stored and all you're

doing is passing an address so that could save you a little performance if you're thinking about like

this is a huge piece of data we're getting back from the database. it's stored in memory now just passed the address around and use the address where where you need it.

 

그러니까 큰 데이터 청크가 있다면 그것을 활용함에 있어 직접 값복사가 이루어지는 것은 성능상에 문제를 일으키므로 포인터를 지정하여 메모리상에 어디에 저장되어 있는지 가리키는 방식으로 활용한다면 성능을 아낄 수 있다는 것이다.

 

말로하면 역시 가물가물하다. 코드로 보자.

package main

import "fmt"

type Data struct {
	value int
	data [200]int // 200개의 원소를 가진 int형 배열
}

func ChangeData(d Data) {
	d.value = 999;
	d.data[100] = 999;
}

func main() {
	var data Data

	// 함수의 인자로 전달되면서 함수 스코프 내의 지역변수로 값 복사(메모리 사용)
	// struct의 크기는 201 * 8 바이트 만큼 할당됨. 근데 이게 다 복사가 된다고? 메모리 낭비 에바임
	// 이 함수 호출될 때마다 복사될거임. 이거 심각함
	ChangeData(data) 

	fmt.Println(data.value)
	fmt.Println(data.data[100])
}

 

위 코드에서 ChangeData를 호출할 때마다 값복사가 일어나므로 메모리를 효율적으로 사용하지 못하게 됩니다.

아래처럼 포인터를 활용하게 되면, 포인터가 값복사 되는데 포인터는 8바이트밖에 안됩니다.

메모리적으로 효율적입니다!!

// 포인터를 받아서 활용함. 포인터는 커봐야 8바이트임. 용량 절약 꿀!
func ChangeData(d *Data) {
    
    // 아니 그럼 *d.value라고 해야하는거 아니냐? 굳이 역참조 안해줘도 go에서는 동작함
	d.value = 999;
	d.data[100] = 999;
}

func main() {
	var data Data

	// 변형할 데이터의 주소값을 넘김. 메모리 주소는 64비트 컴퓨터에선 8바이트, 32비트 컴퓨터에선 4바이트사용
	ChangeData(&data) 

	fmt.Println(data.value) // 999
	fmt.Println(data.data[100]) // 999
}

 

 

 

** 간결하게 포인터를 작성하는 작은 팁

 

아래는 너무 번거롭다.

var data Data
var p *Data
p = &data

 

이러면 한 줄에 곧바로 포인터 변수 할당 가능

var p *Data = &Data{}  
var p *Data = &Data{34, 35} // 이렇게 초기화도 동시에 가능함

 

똑같은 역할을 하는 방식이다. new 내장 함수는 타입을 받고, 해당 타입의 크기만큼 메모리에 할당한 후 주소를 반환한다.

다만 위 방식과 달리 초기화는 못함.

var p = new(Data)

 

 

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체