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

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