본문 바로가기
프로그래밍/Golang

Golang Interface - 포인터와 값 타입의 개념 및 활용

by slowin 2024. 7. 24.

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 포인터 타입

  1. 메모리 사용:
    • 값 타입: 데이터의 전체 복사본을 저장
    • 포인터 타입: 메모리 주소만 저장 (일반적으로 8바이트)
  2. 성능:
    • 값 타입: 작은 데이터의 경우 빠른 접근 가능
    • 포인터 타입: 큰 데이터 구조를 다룰 때 효율적 
      • 참고: 어느 정도 크기의 데이터일 때 포인터를 사용하면 좋은지에 대한 정확한 기준은 없습니다. 최적의 선택은 벤치마크 테스트를 통해 결정하는 것이 좋습니다.
  3. 데이터 수정:
    • 값 타입: 원본 데이터에 영향을 주지 않음
    • 포인터 타입: 원본 데이터를 직접 수정 가능
  4. 함수 호출:
    • 값 타입: 함수에 전달 시 데이터 복사 발생
    • 포인터 타입: 메모리 주소만 전달되어 효율적
    •  

구조체를 사용한 값 타입포인터 타입의 차이점

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
*/

설명:

  1. student: Student 타입의 값 변수입니다.
  2. ps: student의 메모리 주소를 가리키는 포인터입니다.
  3. vs: student의 값을 복사한 새로운 Student 타입 변수입니다.
  4. 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 최적화: 인터페이스 불필요한 추상화 피하고 테스트 용이성 높이기