Go 언어에서의 값 타입과 포인터 타입 소개
Go 언어는 정적 타입 언어로, 값 타입(Value Types)과 포인터 타입(Pointer Types)을 모두 지원합니다.
값 타입 (Value Types)
값 타입은 변수가 직접 값을 저장하는 방식입니다. Go에서 기본적인 값 타입들은 다음과 같습니다:
- 정수형: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
- 부동소수점: float32, float64
- 복소수: complex64, complex128
- 불리언: bool
- 문자열: string
- 배열
- 구조체 (struct)
값 타입의 변수를 다른 변수에 할당하면, 값의 복사가 일어납니다.
x := 5
y := x // y에 x의 값이 복사됨
x = 10 // x의 값만 변경되고, y는 여전히 5
fmt.Printf("x: %d, y: %d\n", x, y)
/* 출력:
x: 10, y: 5
*/
포인터 타입 (Pointer Types)
포인터는 메모리 주소를 저장하는 특별한 변수입니다. 포인터는 값의 위치를 가리키며, 이를 통해 간접적으로 값에 접근하고 수정할 수 있습니다.
포인터 타입은 *T와 같이 표현하며, 여기서 T는 포인터가 가리키는 값의 타입입니다.
포인터와 관련된 주요 연산자:
- &: 주소 연산자. 변수의 메모리 주소를 반환합니다.
- *: 역참조 연산자. 포인터가 가리키는 값에 접근합니다.
x := 5
ptr := &x // ptr은 x의 메모리 주소를 저장
*ptr = 10 // x의 값이 10으로 변경됨
fmt.Printf("x: %d, *ptr: %d\n", x, *ptr)
/* 출력:
x: 10, *ptr: 10
*/
값 타입 vs 포인터 타입
- 메모리 사용:
- 값 타입: 데이터의 전체 복사본을 저장
- 포인터 타입: 메모리 주소만 저장 (일반적으로 8바이트)
- 성능:
- 값 타입: 작은 데이터의 경우 빠른 접근 가능
- 포인터 타입: 큰 데이터 구조를 다룰 때 효율적
- 참고: 어느 정도 크기의 데이터일 때 포인터를 사용하면 좋은지에 대한 정확한 기준은 없습니다. 최적의 선택은 벤치마크 테스트를 통해 결정하는 것이 좋습니다.
- 데이터 수정:
- 값 타입: 원본 데이터에 영향을 주지 않음
- 포인터 타입: 원본 데이터를 직접 수정 가능
- 함수 호출:
- 값 타입: 함수에 전달 시 데이터 복사 발생
- 포인터 타입: 메모리 주소만 전달되어 효율적
구조체를 사용한 값 타입과 포인터 타입의 차이점
package main
import "fmt"
type Student struct {
name string
}
func main() {
student := Student{name: "John"}
ps := &student
vs := student
fmt.Println("초기 상태:")
fmt.Printf("student.name: %s\n", student.name)
fmt.Printf("ps.name: %s\n", ps.name)
fmt.Printf("vs.name: %s\n", vs.name)
ps.name = "Doe"
fmt.Println("\n이름 변경 후:")
fmt.Printf("student.name: %s\n", student.name)
fmt.Printf("ps.name: %s\n", ps.name)
fmt.Printf("vs.name: %s\n", vs.name)
}
/* 출력:
초기 상태:
student.name: John
ps.name: John
vs.name: John
이름 변경 후:
student.name: Doe
ps.name: Doe
vs.name: John
*/
설명:
- student: Student 타입의 값 변수입니다.
- ps: student의 메모리 주소를 가리키는 포인터입니다.
- vs: student의 값을 복사한 새로운 Student 타입 변수입니다.
- ps.name = "Doe": 포인터를 통해 원본 student 객체의 name을 "Doe"로 변경합니다.
요약:
- 포인터 타입 (ps)
- 원본 데이터를 직접 참조합니다.
- 포인터를 통한 변경은 원본 데이터에 영향을 줍니다.
- 메모리 효율적이며, 큰 구조체를 다룰 때 유용합니다.
- 값 타입 (vs)
- 데이터의 복사본을 생성합니다.
- 원본 데이터와 독립적으로 동작합니다.
- 변경해도 원본에 영향을 주지 않습니다.
- 작은 구조체나 변경이 필요 없는 데이터에 적합합니다.
메모리 할당과 참조의 개념
Go 언어에서 메모리 할당과 참조의 개념은 값 타입과 포인터 타입의 동작을 이해해야합니다.
package main
import (
"fmt"
"unsafe"
)
func main() {
// 값 타입
x := 42
y := x
// 포인터 타입
z := &x
fmt.Printf("x의 값: %v, 주소: %p\n", x, &x)
fmt.Printf("y의 값: %v, 주소: %p\n", y, &y)
fmt.Printf("z의 값: %v, 주소: %p, 가리키는 값: %v\n", z, &z, *z)
// 메모리 사용량
fmt.Printf("x의 크기: %d 바이트\n", unsafe.Sizeof(x))
fmt.Printf("z의 크기: %d 바이트\n", unsafe.Sizeof(z))
}
/* 출력 (주소는 실행마다 다를 수 있음):
x의 값: 42, 주소: 0x14000120018
y의 값: 42, 주소: 0x14000120020
z의 값: 0x14000120018, 주소: 0x14000126018, 가리키는 값: 42
x의 크기: 8 바이트
z의 크기: 8 바이트
*/
1. 스택(Stack)과 힙(Heap)
Go에서 메모리 할당은 주로 두 영역에서 이루어집니다:
- 스택(Stack): 함수 호출과 로컬 변수를 위해 사용됩니다. 접근이 빠르고 자동으로 관리됩니다.
- 힙(Heap): 동적으로 할당된 메모리를 저장합니다. 가비지 컬렉터에 의해 관리됩니다..
2. 값 타입의 메모리 할당
값 타입 변수는 일반적으로 스택에 할당됩니다.
x := 42
y := x
- x와 y는 각각 독립적인 메모리 공간을 차지합니다.
- y에 x의 값을 할당할 때 복사가 일어납니다.
- 각 변수는 자신만의 메모리 주소를 가집니다.
3. 포인터 타입의 메모리 할당
포인터는 다른 변수의 메모리 주소를 저장합니다.
z := &x
- z는 x의 메모리 주소를 저장합니다.
- 포인터 자체도 메모리를 차지하며, 일반적으로 8바이트(64비트 시스템 기준)를 사용합니다.
4. 참조의 개념
- 값 타입: 변수는 값 자체를 직접 저장합니다.
- 포인터 타입: 변수는 다른 메모리 위치를 "참조"합니다.
- 포인터를 통한 참조는 원본 데이터에 접근하고 수정할 수 있게 해줍니다.
*z = 100 // x의 값이 100으로 변경됩니다
5. 메모리 효율성
- 큰 구조체나 배열의 경우, 포인터를 사용하면 복사 비용을 줄일 수 있습니다.
- 작은 기본 타입(int, bool 등)의 경우, 값 타입을 사용하는 것이 더 효율적일 수 있습니다.
Go에서 포인터를 사용하는 일반적인 상황
package main
import (
"fmt"
"sync"
)
type User struct {
Name string
Age int
}
func (u *User) Birthday() {
u.Age++
}
func modifyUser(u *User) {
u.Name = "Modified " + u.Name
}
func main() {
// 1. 구조체 메서드에서의 포인터 리시버
user := User{Name: "Alice", Age: 30}
user.Birthday()
fmt.Printf("After Birthday: %+v\n", user)
// 2. 함수에서 구조체 수정
modifyUser(&user)
fmt.Printf("After modifyUser: %+v\n", user)
// 3. 맵에서 구조체 포인터 사용
userMap := make(map[string]*User)
userMap["alice"] = &User{Name: "Alice", Age: 30}
userMap["alice"].Age++
fmt.Printf("User in map: %+v\n", *userMap["alice"])
// 4. 슬라이스에서 구조체 포인터 사용
users := []*User{
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
for _, u := range users {
u.Age += 5
}
fmt.Printf("Users after age increment: %+v, %+v\n", *users[0], *users[1])
}
// 결과
After Birthday: {Name:Alice Age:31}
After modifyUser: {Name:Modified Alice Age:31}
User in map: {Name:Alice Age:31}
Users after age increment: {Name:Bob Age:30}, {Name:Charlie Age:40}
Final count: 1000
정리
Go 언어에서 값 타입과 포인터 타입은 각각 고유한 특성과 사용 사례를 가지고 있습니다. 적절한 타입 선택은 프로그램의 성능, 메모리 사용, 그리고 코드의 명확성에 큰 영향을 미칠 수 있습니다.
관련
Golang Interface - 포인터와 값 타입의 개념 및 활용
Golang Interface - 소프트웨어 인터페이스란?
Golang Interface - OOP에서의 인터페이스 개념, 예시 및 장점
Golang Interface - 암시적 인터페이스: 코드 유연성과 재사용성
Golang Interface - 빈 인터페이스 (interface{})
Golang Interface 최적화: 인터페이스 불필요한 추상화 피하고 테스트 용이성 높이기
'프로그래밍 > Golang' 카테고리의 다른 글
Golang Interface - OOP에서의 인터페이스 개념, 예시 및 장점 (0) | 2024.07.26 |
---|---|
Golang Interface - 소프트웨어 인터페이스란? (0) | 2024.07.25 |
Golang의 struct(구조체)와 receiver(리시버) 메서드: 객체 지향적 설계의 새로운 접근 (0) | 2024.07.23 |
Golang func 와 method (0) | 2024.07.22 |
Golang type(타입) 키워드 탐구 : Named Type과 Type Alias의 차이와 활용 (0) | 2024.07.21 |