Go - 포인터: 메모리 조작에 대한 초보자 가이드

안녕하세요, 미래의 Go 개발자 여러분! 오늘 우리는 Go의 포인터 세계로 흥미로운 여정을 떠납니다. 만약 여러분이 프로그래밍을 한 번도 해본 적이 없다면 걱정 마세요 - 나는 여러분의 친절한 안내자가 되겠습니다. 우리는 단계별로 이를 진행하겠습니다. 이 튜토리얼이 끝나면, 여러분은 Go에서 프로처럼 포인터를 사용할 수 있을 것입니다!

Go - Pointers

포인터는 무엇인가요?

보물지도를 상상해보세요. 보물의 위치에 대한 전체 설명을 적는 대신, 그 좌표만을 적을 수 있습니다. 이것이 프로그래밍에서 포인터의 본질입니다 - 다른 변수의 메모리 주소를 저장하는 변수입니다.

Go에서 포인터는 매우 유용합니다. 그들은 특정 메모리 위치에 저장된 데이터를 직접 조작할 수 있게 해주어, 더 효율적이고 강력한 프로그램을 만들 수 있게 합니다.

간단한 예제로 시작해보겠습니다:

package main

import "fmt"

func main() {
x := 42
p := &x
fmt.Println("x의 값:", x)
fmt.Println("x의 주소:", p)
}

이 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다:

x의 값: 42
x의 주소: 0xc0000b4008

이 예제에서 x는 42 값을 가지는 일반 변수입니다. & 연산자는 x의 메모리 주소를 가져와 p에 저장합니다. 이제 p 변수는 x를 가리키는 포인터가 되었습니다.

포인터를 어떻게 사용할까요?

이제 포인터가 무엇인지 알고 있으므로, 그것을 어떻게 사용할 수 있는지 살펴보겠습니다. 진정한 마법은 포인터가 가리키는 값을 접근하거나 수정하고 싶을 때 발생합니다. 이를 위해 * 연산자를 사용합니다.

package main

import "fmt"

func main() {
x := 42
p := &x

fmt.Println("x의 값:", x)
fmt.Println("p가 가리키는 값:", *p)

*p = 24
fmt.Println("x의 새로운 값:", x)
}

출력:

x의 값: 42
p가 가리키는 값: 42
x의 새로운 값: 24

이 예제에서 우리는 *p를 사용하여 p가 가리키는 값을 접근합니다. 우리는 이 값을 읽을 수도 있고, 변경할 수도 있습니다. *p = 24를 설정할 때, 우리는 실제로 x의 값을 변경하고 있습니다!

포인터 메서드

재미있는 사실 하나: Go에서는 포인터 리시버로 메서드를 정의할 수 있습니다. 이는 메서드가 리시버가 가리키는 값을 수정할 수 있게 해줍니다. 예제를 보겠습니다:

package main

import "fmt"

type Person struct {
Name string
Age  int
}

func (p *Person) HaveBirthday() {
p.Age++
}

func main() {
alice := Person{Name: "Alice", Age: 30}
fmt.Printf("%s는 %d 살입니다\n", alice.Name, alice.Age)

alice.HaveBirthday()
fmt.Printf("생일 후, %s는 %d 살입니다\n", alice.Name, alice.Age)
}

출력:

Alice는 30 살입니다
생일 후, Alice는 31 살입니다

이 예제에서 HaveBirthday 메서드는 포인터 리시버 (p *Person)를 가지고 있습니다. 이는 Person 구조체를 수정할 수 있음을 의미합니다. alice.HaveBirthday()를 호출할 때, Alice의 나이가 1 살 증가합니다.

Go에서의 nil 포인터

친구 중에 영화 보는 밤에 간식을 가져오는 것을 잊는 사람이 한 명 있다면, 포인터도 때로는 아무 것도 가리키지 않을 수 있습니다. Go에서 이를 nil 포인터라고 합니다.

package main

import "fmt"

func main() {
var p *int
fmt.Println("p의 값:", p)

if p == nil {
fmt.Println("p는 nil 포인터입니다")
}
}

출력:

p의 값: <nil>
p는 nil 포인터입니다

nil 포인터에 주의하세요! nil 포인터를 dereference하려고 할 때 (즉, p가 nil일 때 *p를 사용할 때), 프로그램이 빠르게 "segmentation fault"라고 말할 수 있습니다.

Go 포인터의 세부 사항

기본적인 내용을 다루고 나서, 더 심화된 포인터 개념에 대해 조금 더 탐구해보겠습니다.

포인터의 포인터

맞아요, 그렇게 읽었습니다! 포인터가 다른 포인터를 가리킬 수 있습니다. 마치 인ception처럼, 하지만 메모리 주소와 관련이 있습니다.

package main

import "fmt"

func main() {
x := 42
p := &x
pp := &p

fmt.Println("x의 값:", x)
fmt.Println("p가 가리키는 값:", *p)
fmt.Println("pp가 가리키는 값:", **pp)
}

출력:

x의 값: 42
p가 가리키는 값: 42
pp가 가리키는 값: 42

이 예제에서 pp는 포인터의 포인터입니다. **pp를 사용하여 최종적으로 가리키는 값을 접근합니다.

포인터와 슬라이스

Go에서 슬라이스는 이미 포인터의 일종이므로, 흥미로운 행동을 보일 수 있습니다:

package main

import "fmt"

func main() {
slice1 := []int{1, 2, 3}
slice2 := slice1

slice2[0] = 999

fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
}

출력:

slice1: [999 2 3]
slice2: [999 2 3]

이 예제에서 우리는 slice2만을 변경했지만, slice1도 변경됩니다. 이는 슬라이스가 참조 타입이기 때문입니다. 즉, 슬라이스는 배후의 메모리를 가리키는 포인터와 같은 방식으로 동작합니다.

일반 포인터 메서드

다음은 포인터와 관련된 일반적인 연산입니다:

연산 설명 예제
& 변수의 주소를 가져옵니다 p := &x
* 포인터가 가리키는 값을 dereference합니다 y := *p
new() 메모리를 할당하고 포인터를 반환합니다 p := new(int)
make() 슬라이스, 맵, 채널을 생성합니다 s := make([]int, 5)

기억하세요, 실습이 완벽을 만듭니다! 이 개념들을 자신의 코드에서 실험해보세요.

결론적으로, Go의 포인터는 메모리를 직접 조작할 수 있는 강력한 도구입니다. 그들은 우리의 프로그램을 더 효율적이고 복잡한 데이터 구조를 만들 수 있게 합니다. 그러나 그들을 신중하게 다루세요 - 강력한 권한은 큰 책임을 동반합니다!

행복하게 코딩하세요, 여러분의 포인터가 항상 정확하게 가리키기를 바랍니다!

Credits: Image by storyset