1 구조체 기초

Go 언어에서 구조체는 서로 다른 또는 동일한 유형의 데이터를 단일 개체로 집계하는 데 사용되는 복합 데이터 유형입니다. 구조체는 전통적인 객체 지향 프로그래밍 언어와 약간의 차이가 있지만 Go 언어에서 기본적인 객체 지향 프로그래밍의 핵심 요소로 사용됩니다.

구조체의 필요성은 다음과 같은 측면에서 발생합니다:

  • 코드 유지보수성을 향상시키기 위해 강력한 관련성을 가지는 변수를 구성하는 데 필요합니다.
  • "클래스"를 시뮬레이션하는 수단을 제공하여 캡슐화 및 집합 기능을 용이하게 합니다.
  • JSON, 데이터베이스 레코드 등과 상호 작용할 때 구조체는 편리한 매핑 도구를 제공합니다.

구조체를 사용하여 데이터를 구성하면 사용자, 주문 등과 같은 실제 객체 모델을 더 명확하게 나타낼 수 있습니다.

2 구조체 정의

구조체를 정의하는 구문은 다음과 같습니다:

type 구조체이름 struct {
    필드1 타입1
    필드2 타입2
    // ... 다른 멤버 변수
}
  • type 키워드는 구조체의 정의를 소개합니다.
  • 구조체이름은 구조체 타입의 이름으로, Go의 네이밍 규칙에 따라 일반적으로 내보낼 수 있는 것을 나타내기 위해 대문자로 작성됩니다.
  • struct 키워드는 이것이 구조체 유형임을 나타냅니다.
  • 중괄호 {} 내에서 구조체의 멤버 변수(필드)가 각각의 타입과 함께 정의됩니다.

구조체 멤버의 유형은 기본 유형(예: int, string 등) 및 복합 유형(예: 배열, 슬라이스, 또 다른 구조체 등)일 수 있습니다.

예를 들어, 사람을 나타내는 구조체를 정의하는 경우:

type Person struct {
    Name   string
    Age    int
    Emails []string // 슬라이스와 같은 복합 유형을 포함할 수 있음
}

위의 코드에서 Person 구조체는 세 개의 멤버 변수를 가지고 있습니다: 문자열 유형인 Name, 정수 유형인 Age, 그리고 문자열 슬라이스 유형인 Emails, 즉 한 사람이 여러 이메일 주소를 가질 수 있다는 것을 나타냅니다.

3 구조체 생성과 초기화

3.1 구조체 인스턴스 생성

구조체 인스턴스를 생성하는 두 가지 방법이 있습니다: 직접 선언 또는 new 키워드 사용입니다.

직접 선언:

var p Person

위의 코드는 Person 유형의 p 인스턴스를 생성하며, 구조체의 각 멤버 변수는 해당 유형의 제로 값입니다.

new 키워드 사용:

p := new(Person)

new 키워드를 사용하여 구조체를 생성하면 구조체를 가리키는 포인터가 생성됩니다. 이 시점에서 변수 p*Person 유형이며, 구조체의 멤버 변수가 제로 값으로 초기화된 새로 할당된 변수를 가리킵니다.

3.2 구조체 인스턴스 초기화

구조체 인스턴스는 필드 이름을 사용하거나 필드 이름을 사용하지 않는 두 가지 방법으로 한 번에 초기화할 수 있습니다.

필드 이름을 사용하여 초기화:

p := Person{
    Name:   "Alice",
    Age:    30,
    Emails: []string{"[email protected]", "[email protected]"},
}

필드 할당 형식으로 초기화할 때, 초기화의 순서는 구조체를 선언한 순서와 동일할 필요가 없으며, 초기화되지 않은 필드는 제로 값으로 유지됩니다.

필드 이름을 사용하지 않고 초기화:

p := Person{"Bob", 25, []string{"[email protected]"}}

필드 이름을 사용하지 않고 초기화할 때는 각 멤버 변수의 초기값이 구조체를 정의한 순서와 동일한 순서로 있어야하며, 필드를 생략할 수는 없습니다.

또한 구조체는 특정 필드로 초기화할 수 있으며, 지정되지 않은 필드는 각각의 제로 값으로 설정됩니다:

p := Person{Name: "Charlie"}

위 예시에서는 Name 필드만 초기화되고, AgeEmails는 각각 해당하는 제로 값으로 설정됩니다.

4 구조체 멤버 접근

Go에서 구조체의 멤버 변수에 접근하는 것은 매우 간단합니다. 구조체 변수를 가지고 있다면, 해당 멤버 값을 읽거나 수정하는 것이 가능합니다.

패키지 main

수입 "fmt"

형식 사람 struct {
    이름 문자열
    나이 정수
}

기능 main() {
    // Person 유형의 변수 생성
    p := Person{"Alice", 30}

    // 구조체 멤버에 액세스
    fmt.Println("이름:", p.Name)
    fmt.Println("나이:", p.Age)

    // 멤버 값 수정
    p.Name = "Bob"
    p.Age = 25

    // 수정된 멤버 값 다시 액세스
    fmt.Println("\n수정된 이름:", p.Name)
    fmt.Println("수정된 나이:", p.Age)
}

이 예제에서는 먼저 사람 구조체를 이름나이 두 멤버 변수와 함께 정의합니다. 그런 다음 이 구조체의 인스턴스를 생성하고 이러한 멤버를 읽고 수정하는 방법을 보여줍니다.

5 구조체 조합과 내장

구조체는 독립적으로 존재할 뿐만 아니라 더 복잡한 데이터 구조를 만들기 위해 조합 및 중첩될 수 있습니다.

5.1 익명 구조체

익명 구조체는 새로운 유형을 명시적으로 선언하지 않고 구조체 정의를 직접 사용합니다. 이는 구조체를 한 번 생성하고 단순하게 사용해야 하는 경우에 유용합니다.

패키지 main

수입 "fmt"

기능 main() {
    // 익명 구조체 정의 및 초기화
    person := struct {
        이름 문자열
        나이 정수
    }{
        이름: "Eve",
        나이: 40,
    }

    // 익명 구조체의 멤버 액세스
    fmt.Println("이름:", person.이름)
    fmt.Println("나이:", person.나이)
}

이 예제에서는 새로운 유형을 생성하는 대신 구조체를 직접 정의하고 해당 인스턴스를 만드는 방법을 보여줍니다. 이 예제는 익명 구조체의 초기화 및 멤버 액세스 방법을 보여줍니다.

5.2 구조체 내장

구조체 내장은 한 구조체를 다른 구조체의 멤버로 중첩하는 것을 의미합니다. 이를 통해 더 복잡한 데이터 모델을 구성할 수 있습니다.

예제:

패키지 main

수입 "fmt"

// 주소 구조체 정의
형식 주소 struct {
    도시    문자열
    국가 문자열
}

// 사람 구조체에 주소 구조체 내장
형식 사람 struct {
    이름    문자열
    나이     정수
    주소 주소
}

기능 main() {
    // 사람 인스턴스 초기화
    p := 사람{
        이름: "Charlie",
        나이:  28,
        주소: 주소{
            도시:    "뉴욕",
            국가: "미국",
        },
    }

    // 내장된 구조체의 멤버 액세스
    fmt.Println("이름:", p.이름)
    fmt.Println("나이:", p.나이)
    // 주소 구조체의 멤버 액세스
    fmt.Println("도시:", p.주소.도시)
    fmt.Println("국가:", p.주소.국가)
}

이 예제에서는 주소 구조체를 정의하고 이를 사람 구조체의 멤버로 내장합니다. Person의 인스턴스를 만들 때, 동시에 주소의 인스턴스도 만듭니다. 점 표기법을 사용하여 내장된 구조체의 멤버에 액세스할 수 있습니다.

6 구조체 메서드

객체 지향 프로그래밍 (OOP) 기능은 구조체 메서드를 통해 구현할 수 있습니다.

6.1 메서드의 기본 개념

Go 언어에서는 전통적인 클래스 및 객체의 개념은 없지만, 특정 유형의 구조체 (또는 구조체의 포인터)와 결합되는 메서드를 통해 유사한 OOP 기능을 구현할 수 있습니다. 구조체 메서드는 특정 유형에 바인딩되는 특수한 유형의 함수로, 해당 유형에 고유한 메서드 세트를 가질 수 있도록 합니다.

// 간단한 구조체 정의
형식 직사각형 struct {
    길이, 너비 부동소수
}

// 직사각형 구조체에 사각형의 면적을 계산하는 메서드 정의
기능 (r 직사각형) 면적() 부동소수 {
    r.길이 * r.너비
}

6.2 값 수신자와 포인터 수신자

메서드는 수신자의 유형에 따라 값 수신자 및 포인터 수신자로 분류될 수 있습니다. 값 수신자는 메서드를 호출할 때 구조체의 사본을 사용하고, 포인터 수신자는 구조체의 포인터를 사용하여 원본 구조체를 수정할 수 있습니다.

// 값 수신자를 사용하여 메서드 정의
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.length + r.width)
}

// 구조체를 수정할 수 있는 포인터 수신자를 사용하여 메서드 정의
func (r *Rectangle) SetLength(newLength float64) {
    r.length = newLength // 원본 구조체의 값을 수정할 수 있음
}

위 예제에서 Perimeter는 값 수신자 메서드이며, 이를 호출해도 Rectangle의 값이 변경되지 않습니다. 그러나 SetLength는 포인터 수신자 메서드로, 이를 호출하면 원본 Rectangle 인스턴스에 영향을 미칩니다.

6.3 메서드 호출

구조체의 메서드는 구조체 변수와 해당 포인터를 사용하여 호출할 수 있습니다.

func main() {
    rect := Rectangle{length: 10, width: 5}

    // 값 수신자를 사용하여 메서드 호출
    fmt.Println("넓이:", rect.Area())

    // 값 수신자를 사용하여 메서드 호출
    fmt.Println("둘레:", rect.Perimeter())

    // 포인터 수신자를 사용하여 메서드 호출
    rect.SetLength(20)

    // 다시 값 수신자를 사용하여 메서드 호출하면, 길이가 수정된 것에 주목하세요
    fmt.Println("수정 후 넓이:", rect.Area())
}

포인터를 사용하여 메서드를 호출할 때, Go는 메서드가 값 수신자든 포인터 수신자든 상관없이 값을 포인터로 변환하는 작업을 자동으로 처리합니다.

6.4 수신자 유형 선택

메서드를 정의할 때 상황에 따라 값 수신자 또는 포인터 수신자를 사용할지 결정해야 합니다. 일반적인 지침은 다음과 같습니다:

  • 메서드가 구조체의 내용을 수정해야 할 경우 포인터 수신자를 사용하세요.
  • 구조체가 크고 복사 비용이 높을 때 포인터 수신자를 사용하세요.
  • 메서드가 수신자가 가리키는 값을 수정해야 할 경우 포인터 수신자를 사용하세요.
  • 효율성을 위해 구조체의 내용을 수정하지 않더라도 큰 구조체에 대해 포인터 수신자를 사용하는 것이 합리적입니다.
  • 작은 구조체의 경우, 수정이 필요하지 않고 데이터를 읽기만 하는 경우 값 수신자를 사용하는 것이 더 간단하고 효율적입니다.

구조체 메서드를 통해 Go에서는 캡슐화 및 메서드와 같은 객체 지향 프로그래밍의 일부 기능을 모방할 수 있습니다. 이를 통해 Go에서는 객체의 개념을 단순화하면서 관련 함수를 구성하고 관리하는 충분한 기능을 제공합니다.

7 구조체와 JSON 직렬화

Go에서는 네트워크 전송이나 구성 파일로 구조체를 JSON 형식으로 직렬화해야 하는 경우가 많습니다. 마찬가지로 JSON을 구조체 인스턴스로 역직렬화해야 할 때도 있습니다. Go의 encoding/json 패키지는 이러한 기능을 제공합니다.

아래는 구조체와 JSON 간 변환하는 예시입니다:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Person 구조체 정의하고, 구조체 필드와 JSON 필드 이름 간의 매핑을 json 태그를 사용하여 정의합니다
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Person의 새 인스턴스 생성
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// JSON으로 직렬화
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("JSON 직렬화 실패: %s", err)
	}
	fmt.Printf("JSON 형식: %s\n", jsonData)

	// 구조체로 역직렬화
	var p2 Person
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("JSON 역직렬화 실패: %s", err)
	}
	fmt.Printf("복구된 구조체: %#v\n", p2)
}

위 코드에서는 JSON으로 직렬화하기 위해 json.Marshal 함수를 사용하여 구조체 인스턴스를 JSON으로 변환하고, json.Unmarshal 함수를 사용하여 JSON 데이터를 구조체 인스턴스로 역직렬화하였습니다.

구조체 Person을 정의하고 "omitempty" 옵션이 있는 슬라이스 형식 필드를 포함시켰습니다. 이 옵션은 해당 필드가 비어있거나 누락된 경우 JSON에 포함되지 않도록 지정합니다.

8.1 구조체 비교

Go에서는 두 구조체 인스턴스를 직접 비교할 수 있지만, 이 비교는 구조체 내 필드 값에 기반합니다. 모든 필드 값이 동일하면 두 구조체 인스턴스가 동일하다고 간주됩니다. 필드 유형을 모두 비교할 수 있는 것은 아닙니다. 예를 들어 슬라이스를 포함하는 구조체는 직접 비교할 수 없습니다.

아래는 구조체를 비교하는 예시입니다:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // 결과: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // 결과: p1 == p3: false
}

이 예시에서 p1p2는 모든 필드 값이 동일하기 때문에 동일하다고 간주됩니다. 그리고 p3Y 값이 다르기 때문에 p1과 동일하지 않습니다.

8.2 구조체 복사

Go에서는 구조체 인스턴스를 할당으로 복사할 수 있습니다. 이 복사가 깊은 복사인지 얕은 복사인지는 구조체 내 필드의 유형에 달려 있습니다.

만약 구조체가 기본 유형(예: int, string 등)만을 포함하고 있다면, 복사는 깊은 복사입니다. 만약 구조체가 참조 유형(예: 슬라이스, 맵 등)을 포함하고 있다면, 복사는 얕은 복사가 되며, 원본 인스턴스와 새로 복사된 인스턴스는 참조 유형의 메모리를 공유합니다.

다음은 구조체를 복사하는 예시입니다:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// Data 구조체의 인스턴스 초기화
original := Data{Numbers: []int{1, 2, 3}}

// 구조체 복사
copied := original

// 복사된 슬라이스의 요소 수정
copied.Numbers[0] = 100

// 원본과 복사된 인스턴스의 요소 확인
fmt.Println("Original:", original.Numbers) // 결과: Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // 결과: Copied: [100 2 3]

예시에서 보듯이, originalcopied 인스턴스는 동일한 슬라이스를 공유하므로 copied에서 슬라이스 데이터를 수정하면 original의 슬라이스 데이터에도 영향을 미칩니다.

이 문제를 피하려면 슬라이스 내용을 명시적으로 새로운 슬라이스에 복사하여 참조되는 진짜 깊은 복사를 수행할 수 있습니다:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

이렇게 하면 copied를 수정해도 original에 영향을 미치지 않습니다.