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

Go struct와 receiver

by JLearn 2026. 7. 15.
반응형

Go에서는 struct를 함수에 붙이는 것이 아니라, 메서드 선언에 receiver를 지정해 특정 타입과 함수를 연결합니다.

다음 코드를 기준으로 세 요소를 구분할 수 있습니다.

type Product struct {
    Name  string
    Price int
}

func (p Product) Display() string {
    return fmt.Sprintf("%s: %d원", p.Name, p.Price)
}
Product     → struct로 정의한 타입
Display     → Product 타입에 연결된 메서드
(p Product) → 메서드가 동작할 receiver

struct와 일반 함수의 관계

struct는 관련된 데이터를 하나의 타입으로 묶습니다.

type Product struct {
    Name  string
    Price int
}

Product 타입의 값은 다음처럼 생성할 수 있습니다.

product := Product{
    Name:  "키보드",
    Price: 59000,
}

이 시점에서 ProductNamePrice 필드를 가진 데이터 타입입니다.

Product
├── Name
└── Price

일반 함수는 특정 타입에 연결되지 않는 독립된 함수입니다.

func DisplayProduct(p Product) string {
    return fmt.Sprintf("%s: %d원", p.Name, p.Price)
}

호출할 때는 함수 이름에 값을 전달합니다.

result := DisplayProduct(product)
fmt.Println(result)

DisplayProductProduct 값을 매개변수로 받을 뿐, Product 타입의 메서드는 아닙니다.

다른 함수에서도 같은 방식으로 Product 값을 받을 수 있습니다.

func PrintProduct(p Product) {
    fmt.Println(p.Name)
}

func CalculateDiscount(p Product, rate int) int {
    return p.Price * (100 - rate) / 100
}

호출 방식은 다음과 같습니다.

PrintProduct(product)
CalculateDiscount(product, 10)

receiver는 함수를 메서드로 연결합니다

함수 이름 앞에 receiver를 선언하면 해당 함수는 receiver 타입의 메서드가 됩니다.

func (p Product) Display() string {
    return fmt.Sprintf("%s: %d원", p.Name, p.Price)
}

receiver는 다음 두 부분으로 구성됩니다.

p       → 메서드 내부에서 사용할 receiver 변수 이름
Product → 메서드가 연결되는 receiver 타입

이 선언은 다음 의미를 가집니다.

Display 메서드는 Product 타입의 값에 사용할 수 있습니다.
메서드 안에서는 호출 대상 Product 값을 p라는 이름으로 사용합니다.

호출할 때는 값 뒤에 메서드 이름을 붙입니다.

product.Display()

일반 함수와 비교하면 다음과 같습니다.

DisplayProduct(product) // 일반 함수
product.Display()       // 메서드

두 방식 모두 Product 값을 사용하지만 관계는 다릅니다.

DisplayProduct(product)
→ 독립된 함수에 Product 값을 전달

product.Display()
→ Product 타입에 연결된 Display 메서드를 호출

receiver는 특별한 전역 변수가 아닙니다. 메서드 호출 시 동작 대상이 전달되는 변수입니다.

func (p Product) Display() string {
    fmt.Println(p.Name)
    fmt.Println(p.Price)

    return p.Name
}

receiver 이름은 고정되어 있지 않습니다.

func (product Product) Display() string {
    return product.Name
}

다만 Go에서는 짧고 타입과 연관된 이름을 사용하는 경우가 많습니다.

func (p Product) Display() string
func (u User) Name() string
func (c Config) Validate() error

Go에는 Java의 this나 다른 언어의 self처럼 고정된 receiver 이름이 없습니다.

receiver와 일반 매개변수

다음 메서드에는 receiver와 일반 매개변수가 함께 있습니다.

func (p *Product) ChangePrice(price int) error {
    if price < 0 {
        return fmt.Errorf("가격은 0 이상이어야 합니다")
    }

    p.Price = price
    return nil
}
p     → 메서드가 동작할 대상
price → 메서드가 처리할 입력값

호출:

product.ChangePrice(49000)

각 값의 관계는 다음과 같습니다.

product → receiver p
49000   → 매개변수 price
product.ChangePrice(49000)
│       │           │
│       │           └── 변경할 가격
│       └── 수행할 메서드
└── 메서드의 동작 대상

value receiver와 pointer receiver

receiver 타입이 값이면 value receiver이고, 포인터이면 pointer receiver입니다.

value receiver

func (p Product) ChangePrice(price int) {
    p.Price = price
}

호출:

product := Product{
    Name:  "키보드",
    Price: 59000,
}

product.ChangePrice(49000)

fmt.Println(product.Price)

출력:

59000

value receiver의 p는 원본 product의 복사본입니다.

원본 product
┌──────────────────┐
│ Name: 키보드      │
│ Price: 59000     │
└──────────────────┘
          │ 값 복사
          ▼
receiver p
┌──────────────────┐
│ Name: 키보드      │
│ Price: 59000     │
└──────────────────┘

메서드 안에서 p.Price를 변경하면 복사본만 변경됩니다.

원본 product         receiver p
Price: 59000         Price: 49000

따라서 value receiver는 원본 상태를 변경하지 않고 값을 읽거나 계산 결과를 반환하는 메서드에 주로 사용합니다.

func (p Product) Display() string
func (p Product) DiscountedPrice(rate int) int
func (p Product) IsAvailable() bool

pointer receiver

원본 구조체의 상태를 변경하려면 pointer receiver를 사용합니다.

func (p *Product) ChangePrice(price int) {
    p.Price = price
}

호출:

product := Product{
    Name:  "키보드",
    Price: 59000,
}

product.ChangePrice(49000)

fmt.Println(product.Price)

출력:

49000

pProduct 값을 가리키는 포인터입니다.

product
┌──────────────────┐
│ Name: 키보드      │◀────────┐
│ Price: 59000     │         │
└──────────────────┘         │
                             │ 주소
receiver p ──────────────────┘

다음 필드 접근은 포인터가 가리키는 값을 자동으로 역참조합니다.

p.Price = price

개념적으로는 다음과 같습니다.

(*p).Price = price

Go가 구조체 포인터의 필드 접근을 자동으로 처리하므로 일반적으로 p.Price라고 작성합니다.

pointer receiver 호출에서 &를 생략할 수 있는 이유

메서드가 pointer receiver로 선언되어 있어도 주소를 구할 수 있는 변수에서는 다음처럼 호출할 수 있습니다.

product.ChangePrice(49000)

Go는 필요한 경우 다음 호출처럼 처리합니다.

(&product).ChangePrice(49000)

두 호출의 결과는 같습니다.

product.ChangePrice(49000)
(&product).ChangePrice(49000)

반대로 포인터 변수에서 value receiver 메서드를 호출하는 것도 가능합니다.

func (p Product) Display() string {
    return p.Name
}

productPointer := &Product{
    Name: "키보드",
}

fmt.Println(productPointer.Display())

개념적으로는 다음처럼 역참조됩니다.

(*productPointer).Display()

이 자동 변환은 메서드 직접 호출에 적용되는 편의 기능입니다. 인터페이스 구현 여부는 타입의 method set을 기준으로 판단합니다.


메서드는 struct 내부에 저장되지 않습니다

Go에서는 구조체 안에 메서드를 선언하지 않습니다.

// 허용되지 않는 형태
type Product struct {
    Name string

    func Display() string {
        return Name
    }
}

구조체와 메서드는 package block에서 별도로 선언합니다.

type Product struct {
    Name string
}

func (p Product) Display() string {
    return p.Name
}

메서드는 구조체 안에 저장되는 것이 아니라 receiver 타입을 통해 연결됩니다.

type Product struct { ... }
        ▲
        │ receiver 타입으로 연결
        │
func (p Product) Display() ...

같은 package라면 구조체와 메서드를 서로 다른 파일에 작성할 수도 있습니다.

product.go
└── type Product struct

product_method.go
└── func (p Product) Display()

두 파일의 package 선언이 같아야 합니다.

package product

하나의 구조체 타입에는 여러 메서드를 연결할 수 있습니다.

type Product struct {
    Name  string
    Price int
    Stock int
}

값을 읽는 메서드:

func (p Product) Display() string {
    return fmt.Sprintf("%s: %d원", p.Name, p.Price)
}

func (p Product) IsAvailable() bool {
    return p.Stock > 0
}

상태를 변경하는 메서드:

func (p *Product) ChangePrice(price int) {
    p.Price = price
}

func (p *Product) DecreaseStock(quantity int) {
    p.Stock -= quantity
}

사용:

product := Product{
    Name:  "키보드",
    Price: 59000,
    Stock: 10,
}

fmt.Println(product.Display())
fmt.Println(product.IsAvailable())

product.ChangePrice(55000)
product.DecreaseStock(2)

관계는 다음과 같습니다.

Product
├── 필드
│   ├── Name
│   ├── Price
│   └── Stock
└── 연결된 메서드
    ├── Display()
    ├── IsAvailable()
    ├── ChangePrice()
    └── DecreaseStock()

receiver로 사용할 수 있는 타입과 method set

메서드는 구조체에만 정의할 수 있는 것이 아닙니다. 현재 package에서 정의한 이름 있는 타입에도 메서드를 정의할 수 있습니다.

정수 기반 타입:

type Price int

func (p Price) Won() string {
    return fmt.Sprintf("%d원", p)
}

문자열 기반 타입:

type Status string

func (s Status) IsCompleted() bool {
    return s == "completed"
}

slice 기반 타입:

type Products []Product

func (products Products) TotalPrice() int {
    total := 0

    for _, product := range products {
        total += product.Price
    }

    return total
}

다른 package에서 정의된 타입에 직접 메서드를 추가할 수는 없습니다.

// 허용되지 않음
func (t time.Time) CustomFormat() string {
    return ""
}

필요하다면 현재 package에서 새 타입을 정의하거나 구조체로 감쌀 수 있습니다.

type CustomTime time.Time
type CustomTime struct {
    time.Time
}

함수 안에서 선언한 타입

구조체 타입은 함수 안에서도 선언할 수 있습니다.

func main() {
    type Product struct {
        Name string
    }

    product := Product{
        Name: "키보드",
    }

    fmt.Println(product)
}

하지만 메서드 선언은 함수 내부에 작성할 수 없습니다.

func main() {
    type Product struct {
        Name string
    }

    // 허용되지 않음
    func (p Product) Display() string {
        return p.Name
    }
}

함수 안에서 선언한 Product는 함수 밖에서 보이지 않으므로 package block에서 receiver 타입으로 사용할 수도 없습니다.

메서드가 필요한 타입은 일반적으로 package scope에 정의합니다.

type Product struct {
    Name string
}

func (p Product) Display() string {
    return p.Name
}

receiver와 method set

일반 함수는 타입의 method set에 포함되지 않습니다.

func DisplayProduct(p Product) string {
    return p.Name
}

다음 인터페이스가 있어도 위 일반 함수만으로는 Product가 인터페이스를 구현하지 않습니다.

type Displayer interface {
    Display() string
}

인터페이스를 구현하려면 receiver가 있는 메서드가 필요합니다.

func (p Product) Display() string {
    return p.Name
}

value receiver와 pointer receiver는 method set에도 영향을 줍니다.

func (p Product) Display() string {
    return p.Name
}

func (p *Product) ChangePrice(price int) {
    p.Price = price
}

method set은 다음과 같습니다.

타입 method set
Product Display
*Product Display, ChangePrice

다음 인터페이스가 있다고 가정합니다.

type PriceChanger interface {
    ChangePrice(int)
}

ChangePrice가 pointer receiver이므로 *ProductPriceChanger를 구현합니다.

product := Product{}

var changer PriceChanger = &product
changer.ChangePrice(49000)

다음 대입은 컴파일되지 않습니다.

var changer PriceChanger = product

Product의 method set에는 ChangePrice가 없기 때문입니다.

메서드 직접 호출에서 자동 주소 변환이 가능하더라도 인터페이스 구현 여부는 타입의 method set을 기준으로 판단합니다.


정리

다음 코드를 기준으로 전체 관계를 정리할 수 있습니다.

type Product struct {
    Name  string
    Price int
}

func (p *Product) ChangePrice(price int) {
    p.Price = price
}

이 코드는 다음 의미입니다.

  • Product라는 struct 타입을 정의합니다.
  • ChangePrice는 receiver가 있는 함수이므로 *Product 타입에 연결된 메서드가 됩니다.
  • 메서드가 호출되면 Product의 주소가 receiver p에 전달됩니다.
  • 일반 인수로 전달한 값은 price 매개변수에 들어갑니다.
  • p를 통해 원본 Product의 Price 필드를 변경합니다.

호출:

product := Product{
    Name:  "키보드",
    Price: 59000,
}

product.ChangePrice(49000)
product.ChangePrice(49000)
│       │           │
│       │           └── 일반 매개변수 price
│       └── *Product에 연결된 메서드
└── receiver p가 가리킬 원본 값

핵심 관계는 다음과 같습니다.

구분 의미
struct 데이터를 하나로 묶어 새로운 구조체 타입을 정의합니다.
일반 함수 특정 타입에 연결되지 않은 독립적인 동작입니다.
receiver 함수를 특정 타입에 연결하는 역할을 합니다.
method receiver가 선언된 함수입니다.
value receiver receiver 값을 복사해서 전달받습니다.
pointer receiver 원본 값을 가리키는 포인터를 전달받습니다.
method set 특정 타입이 가지고 있는 메서드의 집합입니다.

참고 자료

반응형

댓글