정의
구조체는 여러 필드를 묶어서 하나의 변수처럼 사용합니다. 배열은 같은 타입의 값들로만 연속된 메모리 공간에 저장된다면, 구조체는 다른 타입의 값을 연속된 메모리 공간에 저장됩니다.
정의 및 선언
type 타입명 struct { 필드명 타입 필드명 타입 ... 필드명 타입 }
type 키워드를 사용해서 타입의 명칭을 정의합니다. 타입명의 첫번째 글자가 대문자이면 패키지 외부로 공개되는 타입입니다.
type Singer struct { Name string Group string age int }
이와 같이 관련이 있는 정보들을 하나의 구조체안에 정의해서 넣습니다. 그리고 구조체를 사용해서 예제를 구현해봅니다.
package main import "fmt" type Singer struct { Name string Group string Age int } func main() { var singer Singer singer.Name = "Karina" singer.Group = "Aespa" singer.Age = 24 fmt.Println("이름 : ", singer.Name) fmt.Println("그룹 : ", singer.Group) fmt.Println("나이 : ", singer.Age) }
이런식으로 구조체를 사용하면 프로그램에서 복잡한 자료들을 손쉽게 처리할 수 있습니다.
초기화
전체 필드 초기화
아래처럼 초깃값을 생략하면 모든 필드가 초기화가 됩니다.
var singer Singer
구조체도 타입은 달라질 수 있으나 연속된 메모리 공간이기때문에 배열처럼 초기화가 가능합니다.
// 사용법 1 var singer Singer = Singer { "Karina", "Aespa", 24 } // 사용법 2 var singer Singer = Singer { "Karina", "Aespa", 24, // 마지막 필드에 반드시 쉼표를 달아야 합니다. }
일부 필드 초기화
// 사용법 1 var singer Singer = Singer { Name: "Karina", Group: "Aespa" } // 사용법 2 var singer Singer = Singer { Name: "Karina", Group: "Aespa", }
구조체의 일부 필드만 초기화를 하려면 반드시 초기화 값 옆에 “[필드명]: “을 붙여줘야 합니다.
구조체를 포함하는 구조체
구체의 필드로 다른 구조체를 포함할 수 있습니다. 일반적인 내장 타입처럼 포함하는 방식과 임베디드 필드(Embedded Field) 방식이 있습니다.
내장타입처럼 포함하는 방식
type Singer struct { Name string Age int } type Group struct { Name string Member Singer }
package main import "fmt" type Singer struct { Name string Age int } type Group struct { Name string Member Singer } func main() { singer := Singer{"Karina", 24} // group := Group{"Aespa", Singer{"Karina", 24}} group := Group{"Aespa", singer} fmt.Println("그룹명 : ", group.Name) fmt.Println("가수 : ", group.Member.Name) fmt.Println("나이 : ", group.Member.Age) }
임베디드 필드 방식
위 예제에서는 구조체 안의 구조체 필드에 접근하려면 “.”을 두번을 써서 접근을 해야 했습니다. 이때 임베디드 필드 방식을 쓰면 편리합니다. 구조체안에 포함된 구조체 선언 시에 필드명을 생략하면 됩니다.
package main import "fmt" type Singer struct { SingerName string Age int } type Group struct { GroupName string Singer // 필드명 생략 } func main() { singer := Singer{"Karina", 24} // group := Group{"Aespa", Singer{"Karina", 24}} group := Group{"Aespa", singer} fmt.Println("그룹명 : ", group.GroupName) fmt.Println("가수 : ", group.SingerName) // 내부 필드 접근하듯이 바로 접근하면 된다. fmt.Println("나이 : ", group.Age) // 내부 필드 접근하듯이 바로 접근하면 된다. }
위 예제에서 만약 GroupName과 SingerName을 모두 Name이라는 필드명으로 정의하면 필드명이 충돌이 생기게 됩니다. 이럴때에는 겹치는 필드명일때만 구조체 타입을 명시하고 접근하면 됩니다.
package main import "fmt" type Singer struct { Name string // Group과 Singer가 모두 Name 필드를 가지고 있다. Age int } type Group struct { Name string // Group과 Singer가 모두 Name 필드를 가지고 있다. Singer // 필드명 생략 } func main() { singer := Singer{"Karina", 24} // group := Group{"Aespa", Singer{"Karina", 24}} group := Group{"Aespa", singer} fmt.Println("그룹명 : ", group.Name) // 이렇게 접근하면 자기 자신이 가지고 있는 필드명이 우선이다. fmt.Println("가수 : ", group.Singer.Name) // 내부 구조체의 필드에 접근하려면 구조체 타입명을 명시해야 한다. fmt.Println("나이 : ", group.Age) // 내부 필드 접근하듯이 바로 접근하면 된다. }
구조체 크기
구조체는 Go언어에서 객체지향 프로그래밍을 할때 필수로 사용하는 기능이므로 아주 잘 알고 있어야 합니다. 메모리에 구조체가 어떻게 저장되고, 복사되는지도 알아야 추후에 복잡한 소프트웨어를 구현할때도 잘 할 수 있습니다. 먼저 예제에서 구조체를 하나 만들고 사이즈를 계산해보겠습니다.
package main import ( "fmt" "unsafe" ) type Book struct { Pages int Score float64 } func main() { book := Book{1400, 23.4} fmt.Println(unsafe.Sizeof(book)) }
Go언어에서 int형은 64비트 시스템에서는 int64입니다. 따라서 int형의 크기는 8byte가 되고, float64의 크기도 8byte가 됩니다. 따라서 예제를 실행시켜보면 “16”이 출력 되는 것을 알 수 있습니다. 여기까지는 당연하고, 간단해 보이지만 아래 예제부터는 조금 어렵습니다.
package main import ( "fmt" "unsafe" ) type Book struct { Pages int32 Score float64 } func main() { book := Book{1400, 23.4} fmt.Println(unsafe.Sizeof(book)) }
Pages 필드의 타입을 “int32″로 바꿨습니다. 그렇게 되면 4byte가 되면서 구조체의 사이즈는 12byte가 되어야 하지만 결과는 “16”이 출력됩니다. 이것은 메모리 정렬(Memory Alignment) 때문입니다.
메모리 정렬
메모리 정렬은 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는 것을 말합니다. 64비트 컴퓨터에서는 레지스터 크기가 8byte 입니다. 따라서 한번에 연산을 하려면 8byte가 가장 효율적입니다. 연산을 하기 위해 데이터를 읽어올 때도 8byte 단위로 읽어오는 것이 가장 효율적입니다. 따라서 메모리에 저장할때도, 가장 쉽게 읽어올 수 있게 8의 배수로 시작주소를 설정해서 저장하면 바로 읽어올 수 있습니다.
이런 식으로 메모리 정렬을 위해서 필드 사이에 공간을 띄우는 것을 매모리 패딩(Memory Padding) 이라고 합니다. 4byte 변수의 시작 주소는 4의 배수로 맞추고, 2byte 변수의 시작 주소는 2의 배수로 맞춰서 패딩합니다.
메모리 패딩을 고려한 필드 배치
메모리를 효율적으로 운영하기 위해서 필드 배치를 효율적으로 할 수 있는 방안에 대해 알아보겠습니다.
package main import ( "fmt" "unsafe" ) type Book struct { A int8 // 1byte B int // 8byte C int8 // 1byte D int // 8byte E int8 // 1byte } func main() { book := Book{1, 2, 3, 4, 5} fmt.Println(unsafe.Sizeof(book)) }
결과는 40byte가 됩니다. A, C, E 변수가 7바이트씩 패딩되었기 때문입니다. 하지만 아래와 같이 필드를 수정해보겠습니다.
package main import ( "fmt" "unsafe" ) type Book struct { A int8 // 1byte C int8 // 1byte E int8 // 1byte B int // 8byte D int // 8byte } func main() { book := Book{1, 2, 3, 4, 5} fmt.Println(unsafe.Sizeof(book)) }
결과는 24가 나왔습니다. A, C, E가 1byte씩이어서 8byte안에 넣고, 5byte 패딩한다음 B, D 를 할당했기 때문입니다.
하지만 이런식의 메모리 절약은 메모리가 매우 한정적인 임베디드 개발이 아니라면, 코드 가독성을 중요시하면서 코딩하는 것이 더 나은 방법이라 생각됩니다.
Tucker의 Go 언어 프로그래밍 참조.
책을 보면서 공부한 내용을 정리하면서 작성하는 글입니다. 따라서, 주제 하나를 많은 시간을 들여서 쓰지 않고, 간단하게 작성하는 것부터 시작해서, 계속 다듬어가면서 업데이트해 나갈 생각입니다. 참고하는 자료가 있을 때마다 출처를 적어 놓겠습니다.