Go를 배워보자 9일차 - 구조체, 사용자 정의 타입

0

Go

목록 보기
9/12

구조체

go언어는 도대체 무슨 언어인지 헷갈릴 정도로 이 언어, 저 언어의 특징이 많이 담겨져 있다. 그 중에 하나는 바로 구조체이다.

여태까지, 배열, 슬라이스, 맵은 모두 한개의 값과 한 가지의 타입을 저장할 수 있었다. 그러나 구조체를 사용하면 여러 타입과 여러 값들을 하나의 변수에 저장할 수 있다.

1. 구조체 선언

구조체는 struct 키워드 뒤에 중괄호를 두어 선언할 수 있다. 괄로 안에는 구조체에 함계 그룹핑할 값인 하나 이상의 필드(field)를 선언할 수 있다.

필드 정의는 한 줄에 하나씩 필드의 이름과 값으로 구성된다.

struct {
    field1 string
    field2 int
}

다음의 방식을 사용하며, 구조체는 마치 변수타입처럼 사용할 수 있다.

var temp struct {
		name   string
		age    int
		hasKey bool
		weight float64
	}

temp는 struct로 선언된 변수타입을 가지는 것이다. 보통 이렇게 쓰진 않는다.

  • 도트 연산자(.)를 사용한 구조체 필드 접근
    c언어와 마찬가지로 . 으로 접근 가능하다. class나 js의 object와 같이 도트 연산자로 접근가능하다.
package main

import "fmt"

func main() {
	var temp struct {
		name   string
		age    int
		hasKey bool
		weight float64
	}
	temp.name = "gyu"
	temp.age = 12
	temp.hasKey = false
	temp.weight = 1.2

	fmt.Println(temp.name) // gyu
	fmt.Println(temp.age) // 12
	fmt.Println(temp.hasKey) // false
	fmt.Println(temp.weight) // 1.2
}

다음과 같이 사용가능하다.

2. 구조체를 이용하여 사용자 정의 타입만들기

타입 정의(Type definition)를 사용하면 사용자가 타입을 정의하여 사용할 수 있다. 이는, 기본 타입에 기반한 사용자 정의 타입(defined type)을 만들 수 있다는 것이다.

이를 위해 c언어에서는 typedef를 사용했다. 비슷하게 golang은 type를 사용한다. 마치 typescript 처럼 말이다.

type 타입이름 struct {
    // 필드
}

순서로 쓰면 된다.

역시 특이하게 c언어와 달리 struct 보다 먼저 타입이름을 적어준다.

package main

import "fmt"

type userData struct {
	name   string
	age    int
	hasKey bool
	weight float64
}

func main() {
	var user userData
	user.name = "gyu"
	user.age = 10
	user.hasKey = false
	user.weight = 1.2

	fmt.Println(user)
}

3. 함수에서 사용자 정의 타입 사용하기

일반적인 primitive 타입 변수를 함수에 넘겨주듯이 넘겨주면 된다.

package main

import "fmt"

type userData struct {
	name   string
	age    int
	hasKey bool
	weight float64
}

func nextAge(user userData) int {
	return user.age + 2
}

func main() {
	var user userData
	user.name = "gyu"
	user.age = 10
	user.hasKey = false
	user.weight = 1.2

	fmt.Println(nextAge(user)) // 12
}

그럼 함수에 구조체를 넣고 값을 변경하는 코드를 만들어보자

package main

import "fmt"

type userData struct {
	name   string
	age    int
	hasKey bool
	weight float64
}

func nextAge(user userData) {
	user.age = user.age + 2
	user.name = "park"
	user.hasKey = true
	user.weight = 2.0
}

func main() {
	var user userData
	user.name = "gyu"
	user.age = 10
	user.hasKey = false
	user.weight = 1.2
	nextAge(user)
	fmt.Println(user) // {gyu 10 false 1.2}
}

예상한 결과는 {park 12 true 2.0}이지만, 원하는 결과가 안나올 것이다.
결과는 입력의 결과와 달라지지 않는다. 이유는 함수에 값을 넘기는 것은 pass-by-value이다.

따라서, 함수의 매개변수로 들어가는 것은 입력의 값을 복사할 뿐, 입력 구조체의 메모리 값을 가져오는 것은 아니다.

따라서, 함수에서 구조체를 변경하고 싶다면 포인터를 이용해야한다.

package main

import "fmt"

type userData struct {
	name   string
	age    int
	hasKey bool
	weight float64
}

func nextAge(user *userData) {
	user.age = user.age + 2
	user.name = "park"
	user.hasKey = true
	user.weight = 2.0
}

func main() {
	var user userData
	user.name = "gyu"
	user.age = 10
	user.hasKey = false
	user.weight = 1.2
	nextAge(&user)
	fmt.Println(user) // {park 12 true 2}
}

다음처럼 함수에 user를 넘겨줄때 &로 메모리 값을 넘겨준다. 함수에서는 포인터 *형으로 사용자 정의 타입을 받아 값을 처리해주면 된다.

그런데, 무언가 이상한 점이 있을 것이다. c/c++의 경우 포인터를 사용해서 필드에 접근하기 위해서는 화살표 연산자 ( -> )를 사용해야한다.

하지만 golang은 포인터든 포인터가 아니든, 사용자 정의 타입의 필드에 접근하기 위해서 도트 표기법을 사용하면 된다.

4. 외부 패키지에 따로 타입 정의하기

이전에 패키지 관련 포스팅에서 말했듯이 GOPATH가 있던 곳으로 가서 작업을 해보도록 하자


다음과 같이 폴더를 구성해보자


해당 파일을 만들어보자

  • datas.go
package datas

type User struct {
	Name   string
	Age    int
	HasKey bool
	Weight float64
}
  • main.go
package main

import (
	"datas"
	"fmt"
)

func main() {
	user := generator(10, "park", false, 66.5)
	fmt.Println(user) // {park 10 false 66.5}
}

func generator(age int, name string, hasKey bool, weight float64) datas.User {
	var user datas.User
	user.Age = age
	user.Name = name
	user.HasKey = hasKey
	user.Weight = weight
	return user
}

패키지 "datas"에 User 데이터 타입을 정의해놓았고, main에서 "datas" 패키지를 임포트해보자

그리고 User 데이터 타입을 사용하기위해 패키지.타입을 사용하면 된다. 따라서 datas.User로 타입을 적어준 것이다.

재밌는 것은 구조체 역시도 외부 패키지에서 함수를 불러오듯이 대문자로 써야 부를 수 있다.

type User struct {
	Name   string
	Age    int
	HasKey bool
	Weight float64
}

따라서 User 타입은 앞이 대문자인 것이고, 필드의 맨 앞이 대문자인 것이다. 만약 맨앞이 소문자이면 호출이 불가능하다.

5. 구조체 리터럴

구조체 리터럴을 통해 아주 간단하게 필드를 채워줄 수 있다.

배열이나 슬라이스, 맵과 같이 리터럴 기능을 지원한다.

구조체 리터럴은 map 리터럴과 유사한데 key가 없으므로 key부분이 빠지고 map은 구조체 이름으로 적는다. 이후 값을 넣을 때는 key : value처럼, field : value로 적어야한다.

변수이름 := 구조체이름{field: value, field1 : value2 , ...}

위의 예제에서 main.go만 바꾸어보자

package main

import (
	"datas"
	"fmt"
)

func main() {
	user := datas.User{
		Name:   "park",
		Age:    14,
		HasKey: false,
		Weight: 55.2,
	}
	fmt.Println(user) // {park 14 false 55.2}
}

map 리터럴에서 key만 빼고 map 키워드를 유저 정의 타입으로 쓴다음, 필드 : 값 으로 써주기만 하면 된다.

추가적으로, 구조체 리터럴은 모든 값들을 초기화해줄 필요는 없다. 안써준 값들은 제로값으로 초기화된다.

6. 익명 구조체 필드

구조체 안에 구조체를 만들어서 사용할 수 있다.

datas/datas.go 에 address 타입을 하나 만들어주자

package datas

type User struct {
	Name    string
	Age     int
	HasKey  bool
	Weight  float64
	Address address
}

type address struct {
	First  string
	Street string
	Number int
}

bank/main.go을 다음처럼 바꾸어보자

package main

import (
	"datas"
	"fmt"
)

func main() {
	user := datas.User{
		Name:   "park",
		Age:    14,
		HasKey: false,
		Weight: 55.2,
	}
	user.Address.First = "seoul"
	user.Address.Street = "yonsei"
	user.Address.Number = 10
	fmt.Println(user) //{park 14 false 55.2 {seoul yonsei 10}}
}

다음처럼 user에서 Address를 접근하여 값을 적어주면 된다.

재밌는 것은 golang에서는 익명 구조체 필드를 지원한다. 이는 익명 필드를 정의할 수 있는 것인데, 이름은 없고 타입만 명시하여 구조체 필드를 정의하는 것이다.

익명 필드를 이용하면 훨씬 더 수월하게 내부 구조체에 접근할 수 있다.

단, 익명 구조체 필드를 사용하기 위해서는 해당하는 사용자 정의 타입의 이름은 첫번째 글자가 대문자여야 한다.

따라서, 다음처럼 바꾸도록 하자

  • datas/datas.go
package datas

type User struct {
	Name   string
	Age    int
	HasKey bool
	Weight float64
	Address
}

type Address struct {
	First  string
	Street string
	Number int
}

address 타입을 Address로 정의하여 외부에서도 호출가능하도록 한다. 이렇게 한 이유는 익명 구조체 필드로 쓰게될 경우 대문자로 쓰지 않으면 외부에서 호출이 안되기 때문이다.

만약, 내부(User)에서만 접근하는 구조체라면 굳이 대문자로 써주지 않아도 된다.

그리고 User 타입안에서는 Address 구조체에 대해서 굳이 필드명을 적어줄 필요없이 타입만 적어줘도 된다.

자동으로 필드이름이 Address가 되는 것이다.

  • bank/main.go
package main

import (
	"datas"
	"fmt"
)

func main() {
	user := datas.User{
		Name:   "park",
		Age:    14,
		HasKey: false,
		Weight: 55.2,
	}
	user.Address.First = "seoul"
	user.Address.Street = "yonsei"
	user.Address.Number = 10
	fmt.Println(user) //{park 14 false 55.2 {seoul yonsei 10}}
}

다음과 같이 Address로 접근할 수 있는 것이다.

  • 구조체 임베딩
    익명 필드는 구조체 정의에서 필드명을 생략할 수 있다는 것 이외에도 많은 것들을 제공한다.

외부 구조체의 익명 필드로 선언된 내부 구조체를 외부 구조체 안에 임베딩 되었다고 한다.

임베딩된 구조체의 필드는 외부 구조체로 승격되는데, 승격이라는 말은 내부 구조체의 필드를 마치 외부 구조체에 속해 있는 것처럼 접근할 수 있다는 것을 의미한다.

즉, user.Address.First 로 접근하는 것이 아니라, user.First 로 접근이 가능하다는 것이다. 왜냐하면 익명 필드는 외부 구조체에 임베딩되었고, 임베딩된 내부 구조체는 외부 구조체에 승격되기 때문이다.

만약, 임베딩된 익명 구조체에서의 필드와 외부 구조체의 필드명이 겹치면 어떻게 될까??

가령 User.Address.Number가 있고, User.Number가 있다면 말이다.

이때에는 외부 구조체를 먼저 참조하기 때문에 외부 구조체가 나온다.

  • datas/datas.go
package datas

type User struct {
	Name   string
	Age    int
	HasKey bool
	Weight float64
	Number int
	Address
}

type Address struct {
	First  string
	Street string
	Number int
}

다음과 같이 User 구조체에 익명 필드 Address를 만들어준다. 이 안의 필드로는 Number가 서로 같이 있다.

  • bank/main.go
package main

import (
	"datas"
	"fmt"
)

func main() {
	user := datas.User{
		Name:   "park",
		Age:    14,
		HasKey: false,
		Weight: 55.2,
	}
	user.First = "seoul"
	user.Street = "yonsei"
	user.Number = 20
	user.Address.Number = 10
	fmt.Println(user) //{park 14 false 55.2 20 {seoul yonsei 10}}
}

다음의 예시를 보면 확인할 수 있다. user.Number = 20이라고 입력하면 외부 구조체인 User의 Number 필드를 채운다.

내부의 Number를 채우고 싶다면 user.Address.Number에 채워지면 된다.

first, street 와 같이 외부 구조체 필드와 내부 구조체 필드가 겹치지 않은 경우에는 익명 구조체 사용 시에 user.First , user.Street로 접근할 수 있다.

익명 구조체 필드를 사용하는 것이 무조건 좋은 것은 아니다. 외부에서 사용할 필요가 없고 외부 구조체와 필드가 겹치는 일이 없다면 익명 구조체 필드를 사용하여 임베딩시키는 것이 좋다. 그러나, 그런게 아니라면 고민해보는 것이 좋다.

0개의 댓글