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