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
필드만 초기화되고, Age
와 Emails
는 각각 해당하는 제로 값으로 설정됩니다.
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
}
이 예시에서 p1
과 p2
는 모든 필드 값이 동일하기 때문에 동일하다고 간주됩니다. 그리고 p3
은 Y
값이 다르기 때문에 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]
예시에서 보듯이, original
과 copied
인스턴스는 동일한 슬라이스를 공유하므로 copied
에서 슬라이스 데이터를 수정하면 original
의 슬라이스 데이터에도 영향을 미칩니다.
이 문제를 피하려면 슬라이스 내용을 명시적으로 새로운 슬라이스에 복사하여 참조되는 진짜 깊은 복사를 수행할 수 있습니다:
newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}
이렇게 하면 copied
를 수정해도 original
에 영향을 미치지 않습니다.