구조체
구조체에 대해 알아보자.
구조체
구조체는 '하나 이상의 변수를 묶어서 새로운 자료형을 정의하는 Custom data type'
입니다. Go언어에서의 구조체는 필드들의 집합체이며 필드들의 컨테이너
입니다. C언어와 같은 절차 지향 언어에서 구조체는 객체들의 공통되는 속성과 특징
에 따라 쓰입니다. 즉, '정보 집합'
이라는 것입니다.
하지만, Go언어는 객체 지향
을 따른다고 했습니다. 여기서 주의해야 할 것은 Go언어는 전통적으로 객체 지향 언어의 클래스, 객체, 상속 개념이 없다
는 것입니다.
Go언어에서 클래스는 Custom 타입을 정의하는 구조체로 표현
되는데, 이는 전통적인 객체지향의 클래스가 필드와 메소드를 함께 갖는 것과 달리 구조체는 필드만
을 가지고, 메소드는 별도로 분리
하여 정의하는 것입니다. 구조체는 Custom data type이기 때문에 type문을 사용해서 구조체를 정의합니다. 아래에 구조체의 선언 형태를 알아보기 위해 사람의 정보를 만들 수 있는 구조체를 만들었습니다.
type person struct {
name string
age int
contact string
}
구조체 객체를 생성하려면 "객체이름 := 구조체이름{저장할값}"
으로 입력해 선언과 동시에 초기화를 할 수 있습니다. 그리고 빈 객체 혹은 만약 일부 필드가 생략될 경우 생략된 필드들은 Zero value를 갖습니다.
package main
import "fmt"
type person struct {
name string
age int
contact string
}
func main() {
var p1 = person{}
fmt.Println(p1)
p1.name = "kim"
p1.age = 25
p1.contact = "01000000000"
fmt.Println(p1)
p2 := person{"nam", 31, "01022220000"} // 필드 이름을 생력할 시 순서대로 저장함
fmt.Println(p2)
p3 := person{contact: "01011110000", name: "park", age: 23} // 필드 이름을 명시할 시 순서와 상관 없이 저장할 수 있음
fmt.Println(p3)
p3.name = "ryu" //필드에 저장된 값을 수정할 수 있음
fmt.Println(p3)
fmt.Println(p3.contact) //필드 값의 개별 접근도 가능함
}
Go언어의 구조체는 기본적으로 'mutable' 개체
로서 필드 값이 변화할 경우 별도로 새 개체를 만들지 않고 해당 개체 메모리에서 직접 변경됩니다. 위 코드에서 p3.name = “ryu”를 입력해 값을 직접 수정했습니다.
그리고 함수(메소드)에서 같이 값을 복사해서 지역 변수로 사용하는 경우
가 아니라 원래 값의 주소를 참조해 값이 저장된 주소에 직접 접근 하는 경우
에 포인터를 썼습니다. 매개변수에 ‘&’을 붙여서 Pass by reference를 한 것입니다. 구조체도 마찬가지로 '구조체 포인터'
를 생성할 수 있습니다. 구조체 포인터를 생성하는 방법은 두 가지가 있습니다.
1. 'new(구조체이름)'을 사용하여 객체를 생성하기.
2. 구조체 이름 앞에 & 붙이기.
주의할 점은, 다른 자료형의 포인터들은 역참조를 위해 '*' 연산자를 사용
했습니다. 하지만 포인터 구조체는 선언하면 자동으로 역참조
됩니다. 따라서 함수 안에서 * 연산자를 사용할 필요가 없습니다.
package main
import "fmt"
type person struct {
name string
age int
contact string
}
func addAgeRef(a *person) { //Pass by reference
a.age += 4 //자동 역참조 * 생략
}
func addAgeVal(a person) { //Pass by value
a.age += 4
}
func main() {
var p1 = new(person) //포인터 구조체 객체 생성
var p2 = person{} // 빈 구조체 객체 생성
fmt.Println(p1, p2)
p1.age = 25
p2.age = 25
addAgeRef(p1) //&을 쓰지 않아도 됨
addAgeVal(p2)
fmt.Println(p1.age)
fmt.Println(p2.age)
addAgeRef(&p2) //&을 붙이면 pass by reference 가능
fmt.Println(p2.age)
}
생성자(constructor) 함수
때로는 구조체의 필드 자체가 사용 전에 초기화되어야 하는 경우가 있습니다. 예를 들어, 구조체의 필드가 'map'(컬랙션) 형일 경우
구조체를 초기화할 때마다 맵 필드도 같이 초기화
해야 하는 번거로움이 있을 수 있습니다. 따라서 사전에 미리 초기화를 해 놓으면 외부 구조체 사용자가 매번 맵을 초기화해야 한다는 것을 기억할 필요가 없습니다
이러한 목적을 위해 ‘생성자 함수’를 사용할 수 있습니다. 생성자 함수는 호출하면 구조체 객체 생성 및 초기화, 입력한 필드 생성 및 초기화함과 동시에 구조체를 반환
합니다.
type mapStruct struct{ //맵 형태의 data필드를 가지는 "mapStruct"를 정의함
data map[int]string
}
func newStruct() *mapStruct { //포인터 구조체를 반환함
d := mapStruct{} //구조체 객체를 생성하고 초기화함
d.data = map[int]string{} //data 필드의 맵을 초기화함
return &d //초기화 한 포인터 구조체를 반환함
}
위 생성자 함수는 구조체 객체를 포인터와 함께 반환합니다. 포인터 값이 없는 객체를 생성하는 생성자를 만들려면 반환형에 구조체 이름 앞에 붙은 포인터 연산자를 없애면 됩니다.
package main
import "fmt"
type mapStruct struct {
data map[int]string
}
func newStruct() *mapStruct { //포인터 구조체를 반환함
d := mapStruct{}
d.data = map[int]string{}
return &d
}
func main() {
s1 := newStruct() // 생성자 호출
s1.data[1] = "hello"
s1.data[10] = "world"
fmt.Println(s1)
s2 := mapStruct{}
s2.data = map[int]string{}
s2.data[1] = "hello"
s2.data[10] = "world"
fmt.Println(s2)
}
메소드
특정 속성들의 기능을 수행하기 위해 만들어진 특별한 함수
를 ‘메소드’라고 합니다. Java에서는 이들을 한 곳에 묶은 클래스 안에 필드와 메소드
가 있습니다. 하지만 말했다시피 Go언어에서는 구조체 내에서 메소드를 선언하지 않고 일반 함수처럼 별도로 분리되어 선언
됩니다. 쉽게 말하자면 메소드는 구조체의 필드들을 이용해 특정 기능을 하는 특별한 함수입니다.
1. 기본적으로 메소드는 "func (매개변수이름 구조체이름) 메소드이름() 반환형 {" 형식으로 선언합니다. 매개변수 이름은 구조체 변수명으로서 메소드 내에서 매개변수처럼 사용됩니다.
2. '함수이름'을 입력하지 않고 구조체이름 뒤에 메소드 이름을 입력합니다. 본문에서 메소드를 이용하기 위해 이름을 사용합니다.
package main
import "fmt"
type triangle struct {
width, height float32
}
func (s triangle) triArea() float32 { //value receiver
return s.width * s.height / 2
}
func main() {
tri1 := new(triangle)
tri1.width = 12.5
tri1.height = 5.2
triarea := tri1.triArea()
fmt.Printf("삼각형 너비:%.2f, 높이:%.2f 일때, 넓이:%.2f ", tri1.width, tri1.height, triarea)
}
위 코드에서 (s triangle)은 어떤 구조체를 전달 받는지 명시
하는 ‘receiver’입니다. 구조체 객체 자체를 전달받는 것이 아니라 구조체 객체 정보를 전달 받고 메소드의 기능을 수행하는 것입니다.
Value Receiver와 Pointer Receiver
위 코드는 구조체의 '값' 정보를 전달(복사) 받아 연산한 후 반환
합니다.
하지만 포인터는 정보를 전달한다면 구조체 필드 값을 메소드에서 직접 접근해 수정
할 수 있습니다. 메소드의 receiver 부분에서 주솟값을 참조하는 연산자인 ‘*‘를 구조체 이름 앞에 붙여주면 됩니다.
package main
import "fmt"
type triangle struct {
width, height float32
}
func (s triangle) triAreaVal() float32 { //Value Receiver
s.width += 10
return s.width * s.height / 2
}
func (s *triangle) triAreaRef() float32 { //Pointer Reciver
s.width += 10
return s.width * s.height / 2
}
func main() {
tri1 := new(triangle)
tri1.width = 12.5
tri1.height = 5.2
triarea_val := tri1.triAreaVal()
fmt.Printf("삼각형 너비:%.2f, 높이:%.2f 일때, 넓이:%.2f \n", tri1.width, tri1.height, triarea_val)
triarea_ref := tri1.triAreaRef()
fmt.Printf("삼각형 너비:%.2f, 높이:%.2f 일때, 넓이:%.2f ", tri1.width, tri1.height, triarea_ref)
}