구조체 기초

Struct basic

구조체는 이름을 갖는 엘리먼트들을 묶어 놓은 타입이다. 당연한 얘기를 뭐 대단한 듯 하나 싶겠지만 앞서 다뤘던 기본 타입, 포인터, 함수, 아직 다루지 않은 배열, 슬라이스, 맵, 채널 등과 함께 구조체도 타입이며, 따라서 타입이 쓰이는 자리에 쓸 수 있다는 점은 짚고 넘어가자.

기본 사용법

보통 Go 언어 학습서의 구조체 사용법은 이 순서가 아니지만, 위에서 구조체도 타입이라는 점을 강조했으니 순서를 좀 바꿔보자.

변수로 돌아가서 기본 타입 변수를 선언하는 방법을 보자.

var i int
var f float32

int, float32 등 타입은 선언할 변수명 뒤에 썼다. 구조체도 타입이므로 이 자리에 사용할 수 있다. 구조체를 작성하는 방법은 다음과 같다.

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .

FieldDecl 중 아직 다루지 않을 EmbeddedField, Tag에 대해서는 일단 잊자. 타입 자리에 쓴다는 사실과 위 작성 규칙에 따라 구조체는 다음과 같이 사용할 수 있다.

var myStruct struct {
	i int
	f float32
	s string
}

구조체 내 각 필드에 값을 쓰거나 읽을 때는 .을 사용한다.

myStruct.i = 10
myStruct.f = 3.14
myStruct.s = "Hello"

fmt.Println(myStruct.i)
fmt.Println(myStruct.f)
fmt.Println(myStruct.s)

기본 타입은 변수 선언과 동시에 값을 할당할 수도 있었다.

var i int = 10
var j = 20
k := 30

구조체 역시 가능한데 조금 복잡하다. 우선 기본 타입에 대해 좀 더 구체적으로 살펴보면,

VarDecl        = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec        = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

값을 할당하기 위해서는 = 뒤에 Expression이 온다.

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .
PrimaryExpr =
	Operand |
	Conversion |
	MethodExpr |
	PrimaryExpr Selector |
	PrimaryExpr Index |
	PrimaryExpr Slice |
	PrimaryExpr TypeAssertion |
	PrimaryExpr Arguments .
Operand     = Literal | OperandName | "(" Expression ")" .
Literal     = BasicLit | CompositeLit | FunctionLit .
BasicLit    = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .

기본 타입은 위와 같은 규정에 따라 최종적으로 int_lit, float_lit 등으로 값을 할당한다. 구조체의 경우 Literal 중 CompositeLit에 해당하는 방식으로 선언과 함께 값을 할당하는데 다음과 같다.

CompositeLit  = LiteralType LiteralValue .
LiteralType   = StructType | ArrayType | "[" "..." "]" ElementType |
                SliceType | MapType | TypeName .
LiteralValue  = "{" [ ElementList [ "," ] ] "}" .
ElementList   = KeyedElement { "," KeyedElement } .
KeyedElement  = [ Key ":" ] Element .
Key           = FieldName | Expression | LiteralValue .

타입 뒤에 중괄호({})로 묶어 값을 입력하는데 각 필드는 comma(,)로 구분하며, 필드의 이름을 Key로 사용할 수도 있고 생략할 수도 있다. 즉, 위 myStruct 변수를 선언과 동시에 값을 할당하기 위해서는 다음과 같이 쓸 수 있다.

var myStruct struct {
	i int
	f float32
	s string
} = struct {
	i int
	f float32
	s string
} {10, 3.14, "Hello"}

// or

var myStruct struct {
	i int
	f float32
	s string
} = struct {
	i int
	f float32
	s string
} {i:10, f:3.14, s:"Hello"}

타입 선언

위 방식은 대강 봐도 굉장히 복잡할뿐 아니라 재사용성 측면에서도 활용도가 0에 가깝다. 구조체는 새로운 타입을 만들기 위해 사용하는데, 새로운 타입을 만들었으면 당연히 재사용할 수 있어야 할 것 아닌가.

타입 선언 방법

Go는 기존 타입에 새로운 이름을 붙이는 방법을 제공한다. C의 typedef를 생각하면 된다. C에서 typedef를 사용해 같은 int 이면서도 용도에 따라 이름을 달리 하거나, 구조체에 별칭을 붙이는 것처럼 Go도 유사한 방식을 사용할 수 있다.

typedef int network_error;
typedef int protocol_error;
typedef int internal_error;

network_error doNetwork();
protocol_error doProtocol();
internal_error doInternal();

typedef struct _MyStruct
{
	int a;
	char b;
} MyStruct;

Go의 타입 선언 규칙은 다음과 같다.

TypeDecl  = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec  = AliasDecl | TypeDef .
AliasDecl = identifier "=" Type .
TypeDef   = identifier Type .

AliasDecl과 Typedef는 중간에 =가 있으냐 없느냐의 차이만 있다. AliasDecl은 주로 이미 이름이 있는 타입에 별칭을 추가할 때, TypeDef는 구조체 타입과 같이 아직 이름이 없는 타입에 이름을 붙일 때 사용하지만 AliasDecl 방식을 이용해 구조체 타입에 이름을 붙일 수도 있고, TypeDef 방식을 이용해 기본 타입에 별칭을 추가할 수도 있다.

AliasDecl과 TypeDef의 차이

위에서는 두 방식에 = 외 별 차이가 없다고 했지만, 사실 두 방식에는 큰 차이가 있다.

우선 Go spec.에서 두 방식에 대해 설명하고 있는 문구를 보자.

Alias declarations

An alias declaration binds an identifier to the given type.

Type definitions

A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.

AliasDecl은 이름 그대로 주어진 타입에 대한 별칭을 만드는 것이고, TypeDef는 새로운 타입을 만드는 것이다. 이 둘의 차이는 다음 예를 보면 쉽게 구분할 수 있다.

package main

import "fmt"

type myint int

func test(a int) {
	fmt.Println(a)
}

func main() {
	var m myint = 1
	test(m)
}

이 코드는 다음과 같은 오류가 발생한다.

./main.go:13:6: cannot use m (type myint) as type int in argument to test

myintint가 아닌 새로운 타입이기 때문에 test() 함수에 그대로 인수로 전달할 수 없다. 하지만 myint를 다음과 같이 AliasDecl로 변경하면 이 코드는 문제없이 정상 동작한다.

package main

import "fmt"

type myint = int

func test(a int) {
	fmt.Println(a)
}

func main() {
	var m myint = 1
	test(m)
}

타입 선언을 이용한 구조체 변수 선언과 초기값 할당

이제 타입 선언법을 알았으니 위에서 살펴본 myStruct 변수의 구조체에 적용해 보자.

type MyStruct struct {
	i int
	f float32
	s string
}

var myStruct MyStruct

동시에 값을 할당할 때는 다음과 같은 방식들을 사용할 수 있다.

var myStruct MyStruct = MyStruct{10, 3.14, "Hello"}
var myStruct = MyStruct{10, 3.14, "Hello"}
myStruct := MyStruct{s:"Hello", i:10, f:3.14}