[Go] struct

김무연·2025년 1월 22일

Struct (구조체)

Go에서 struct는 Custom Data Type을 표현하는데 사용되는데, Go의 struct는 필드들의 집합체이며 필드들의 컨테이너이다.
Go에서 struct는 필드 데이터만을 가지며, (행위를 표현하는) 메서드를 갖지 않는다.

  • Go 언어는 객체지향 프로그래밍(Object Oriented Programming, OOP)을 고유의 방식으로 지원한다. 즉, Go에는 전통적인 OOP 언어가 가지는 클래스, 객체, 상속 개념이 없다.
  • 전통적인 OOP의 클래스(class)는 Go 언어에서 Custom 타입을 정의하는 struct로 표현되는데, 전통적인 OOP의 클래스가 필드와 메서드를 함께 갖는 것과 달리 Go 언어의 struct는 필드만을 가지며, 메서드는 별도로 분리하여 정의한다

Struct 선언

  • struct를 정의하기 위해서는 Custom Type을 정의하는데 type문을 사용한다. 만약 이 구조체를 패키지 외부에서도 사용할 수 있게 하려면 함수처럼 앞 글자를 대문자로 변경해주면 된다.
  • 선언된 struct로 객체를 생성하는 법은, 아래와 같다
  1. 선언된 struct로 {} 빈 객체를 생성한 후 필드값을 채워주기, 마찬가지로 이때 채워주지 않은 필드값은 zero value가 된다.
  2. 초기값을 함께 할당하려면 빈 객체가 아닌, 필드값을 순차적으로 넣어주면 된다. 마찬가지로 할당하지 않은 필드값은 zero value
  3. Go 내장함수 new()를 이용한다. new()를 이용하면 모든 필드를 zero value로 초기화하고 해당 객체의 포인터(*person)를 리턴한다. 객체 포인터인 경우에도 필드 엑세스시 .(dot)을 이용하는데, 이때 포인터는 자동으로 dereference(역참조) 된다. (이는 C에서 포인터의 -> 문법과는 다르다
  • 역참조: 주소를 통해 그 값에 접근하는 것
  1. Go에서 struct는 기본적으로 mutable(변하기 쉬운) 개체로서 필드값이 변화할 경우 (별도로 새 개체를 만들지 않고) 해당 개체 메모리에서 직접 변경된다.
  • 하지만 struct개체를 다른 함수의 파라미터로 넘긴다면, pass by value에 따라 객체를 복사해서 전달하게 된다.
  • 그래서 만약 pass by reference로 struct를 전달하고자 한다면, struct의 포인터를 전달해야 한다

선언 예시 코드

package main

import (
	"fmt"
)

// 다른 패키지에서도 쓰려면 앞글자 대문자
type person struct {
	name string
	age int
	gender bool
}

func main() {
	// 객체 생성
	p := person{}

	// 필드값 설정
	p.name = "김두한"
	p.age = 10
	p.gender = true

	// 초기화와 동시에 선언해주어도 된다, 
	// struct의 값을 지정안해줄 경우 zero value
	// 항상 구조체는 선언되어 있어야 할 것
	p2 := person{
		name : "구마적",
		age : 25,
	}

	// 내장함수 new()를 이용해서 선언, 모든 필드 zero value

	fmt.Printf("%+v\n",p)
	fmt.Printf("%+v\n", p2)
}

struct 메모리 크기

struct에 할당하는 메모리의 크키는 어떻게 될까?

  • string은 16바이트, int는 8바이트, bool은 1바이트가 된다. 그렇다면 단순히 16 + 8 + 1 로 생각해서 25바이트라고 생각할순 있겠지만, 실제로는 32바이트를 할당한다.
  • 이를 이해하려면 패딩과 정렬을 알아야 한다.

정렬 : 하드웨어에게는 정렬 경계내의 메모리를 일게 하는 것이 효율적이다. 하드웨어가 정렬 경계에 맞춰 읽게 소프트웨어에서 챙겨주는 것이 정렬이다.

특정값의 메모리 크기에 따라 Go는 어떤 정렬이 필요한지 결정한다. 모든 2바이트 크기의 값은 2바이트 경계를 가진다. bool값은 1바이트라서 주소 0번지에서 시작했으면, 다음 int는 2번지에서 시작해야 한다.

  • 메모리 정렬은 가장 큰 데이터 타입의 크기에 따라 경계가 정해진다. 즉 가장 큰 메모리 사이즈의 필드가 전체 구조체의 패딩을 결정하게 된다.
  • 위와 같은 경우 string이 가장 큰 데이터 타입이기 때문에 복합 타입의 string의 최고 타입인 int가 정렬 경계가 되어 8바이트가 경계가 된다. 때문에 0번지에 16바이트가 할당되고 그 이후 int가 8바이트를 차지를 한 후 bool이 1바이트를 차지를 하면 8바이트 경계에 7바이트가 남게 된다. 이 공간을 패딩이라고 한다.

이름이 있는 타입과 익명 타입 (Named type VS Anonymous type)

  • 두 구조체 타입의 필드가 완전히 같아도, 한 타입의 구조체 변수를 다른 타입의 구조체 변수에 대입할 수 없다.
  • 예를 들어 동일한 두 구조체를 가지는 test1, test2 타입이 있을 때, test1 = test2 대입은 허용되지 않는다.
  • result1 = test1(result2) 라고 명시적인 변환(conversion)을 해주어야 한다.
package anonymoustype

import "fmt"

type test1 struct {
	a int
	b int
}

type test2 struct {
	a int
	b int
}

func Anonymoustype() {
	var result1 test1
	var result2 test2

	result1 = test1(result2)


	fmt.Printf("같냐? = %v\n",result1 )

}
  • 허나 동일한 구조의 익명 구조체 타입인 경우에는 명시적 형변환이 없더라도 대입이 가능하다
package anonymoustype


func Namedtyped() {
	type Namedtype struct {
		count int
	}
	
	var test2 struct {
		count int
	}


	var test1 Namedtype

	test1=test2
}

위 코드에서 test2 변수에 담긴 구조체는 현재 이름이 선언되어 있지 않는 익명 구조체 타입이다. 이를 Namedtype이라는 이름을 가진 구조체를 타입으로 하는 test1에 test2구조체를 대입하니 성공적으로 대입이 된다.

profile
Notion에 정리된 공부한 글을 옮겨오는 중입니다... (진행중)

0개의 댓글