[Go] 07. 배열, 구조체

2024. 1. 17. 17:00Go

반응형

1. 배열

같은 타입의 데이터들로 이루어져 있습니다.

배열을 이루는 각 값을 elemenet(요소), 위치값은 index라고 합니다.

var 변수명 [요소 개수]타입

  • 배열 요소수는 상수 값만 설정할 수 있습니다.
  • 배열의 길이는 len(배열명) 으로 구할 수 있습니다.
  • range를 이용하면 for순회하면서 인덱스와 값을 함께 조회할 수 있습니다.
  • 배열의 길이는 설정하였으나 값을 설정하지 않거나 구조체 변수의 각 필드값을 초기화 하지 않을 경우, 각 데이터 타입의 기본값으로 초기화 됩니다.
  • 배열이나 구조체와 같이 중괄호를 이용하여 초기값을 설정하는 변수들은 }가 새로운 줄에 위치하지 않을 경우 반드시 , 를 작성해줘야합니다.

1.1. 배열 초기화

arr := [5]float32{3.14, 5.9, 9.0, 7, 12.3}  

for i := 0; i < len(arr); i++ {  
  fmt.Println(arr[i])  
}
3.14
5.9
9
7
12.3

실수의 경우, format을 지정하지 않으면 최소 소수점 까지 표시하기 때문에 9.0, 7은 모두 9, 7로 출력됩니다.


배열을 선언하고 초기화를 하지 않는다면, 각 데이터 타입에 따른 기본값으로 초기화됩니다. (정수 데이터의 경우에는 0)

var arr [3]int  
fmt.Println(arr)
[0 0 0]

아래와 같이, 배열의 길이에 비해 초기화에 이용되는 요소수가 적을 경우에는, 맨 앞 인덱스부터 할당되고 나머지는 데이터타입에 맞는 기본값이 설정됩니다.

arr := [5]int{5, 2}  
fmt.Println(arr)
[5 2 0 0 0]

아래와 같이 `인덱스:값` 방식으로 배열을 초기화하면 특정 인덱스에만 요소를 초기화할 수도 있습니다.
arr := [5]int{2: 11, 4: 98}  
fmt.Println(arr)
[0 0 11 0 98]

배열 요소 개수를 생략하고 싶다면 [...] 를 사용하면 됩니다.

arr := [...]string{"red", "blue", "green"}  
fmt.Println(arr, len(arr))
[red blue green] 3

1.2. range

for range 를 이용하면 배열을 순회하면서 인덱스, 값 을 조회할 수 있습니다.

range는 array외에도 slice, string, map, channel에도 사용할 수 있습니다.

arr := [...]string{"red", "blue", "green"}  
for i, v := range arr {  
  fmt.Println(i, v)  
}
0 red
1 blue
2 green

만약, 인덱스가 필요없다면, 아래와 같이 작성하면 됩니다.
for _, v := range { }


1.3. 배열 주소

arr := [...]int16{11, 12, 13}  
for i := 0; i < len(arr); i++ {  
  fmt.Printf("인덱스: %d, 값: %d, 주소값: %d\n", i, arr[i], &arr[i])  
}
인덱스: 0, 값: 11, 주소값: 1374390206482
인덱스: 1, 값: 12, 주소값: 1374390206484
인덱스: 2, 값: 13, 주소값: 1374390206486

배열은 연속된 메모리공간에 저장되기 때문에, 각 요소의 주소는 요소의 데이터타입 크기에 따라 달라집니다.
배열 주소는 1바이트(= 8bit)당 1차이가 납니다.

즉, int8타입의 경우, 각 배열요소의 주소차이가 1이고
int16타입은 2, int64는 8씩 차이가 납니다.


1.4. 배열 복사

데이터타입과 배열 크기가 같은 경우,
아래와 같이 배열간의 대입 방식(a = b )으로 b에 들어있는 값을 a 배열의 각 메모리 공간에 복사할 수 있습니다.

복사 시, 각 배열의 주소값은 변하지 않습니다.

a := [3]int{10, 20, 30}  
b := [3]int{1, 2, 3}  
fmt.Println(a)  
fmt.Println(b)  

fmt.Println("-----------")

a = b  
fmt.Println(a)  
fmt.Println(b)
[10 20 30] 0x140000a8018
[1 2 3] 0x140000a8030
-----------
[1 2 3] 0x140000a8018
[1 2 3] 0x140000a8030

1.5. 다중 배열

데이터타입이 int이면서, 요소가 5개인 배열이 2개인 이중배열은 표현하는 방법은 아래와 같습니다.

var arr [2][5]int

배열을 닫는 } 를 배열 항목과 같은 줄에 작성할 때에는 , 를 생략해도 되지만

a := [2][5]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}}

다른 줄에 있을 때에는 반드시 , 를 작성해야 합니다.

배열 외에도 구조체 등과 같이 괄호를 이용하여 값을 초기화할 수 있는 데이터타입들은 이와 같은 특징을 가지고 있습니다.

a := [2][5]int{  
  {1, 2, 3, 4, 5},  
  {6, 7, 8, 9, 10},  
}

기본 배열에서 아래와 같이 작성할 경우에는 ,를 찍어줘야 합니다.

b := [...]int{1, 2, 3, 4, 5,
}

간단하게, 위의 배열을 이용한 예제를 확인해봅시다.

a := [2][5]int{  
  {1, 2, 3, 4, 5},  
  {6, 7, 8, 9, 10},  
}  

fmt.Println(a)  
fmt.Println(a[0])  
fmt.Println(a[1])
[[1 2 3 4 5] [6 7 8 9 10]]
[1 2 3 4 5]
[6 7 8 9 10]

a[0]에 해당하는 값이 바로 {1, 2, 3, 4, 5} 입니다.


2. 구조체

  • 구조체의 크기는 구조체 내에 정의된 필드들의 데이터크기를 모두 합산한 값입니다.
    • int타입 필드 1개와 float64타입 필드 1개로 구성되어있다면 8byte + 8byte = 16byte
  • (배열과 같이) 대입연산으로 구조체 값 복사가 가능합니다.
    • m1 = m2

2.1. 구조체 선언

(타입이 같거나 다른) 여러개의 변수를 하나로 묶은 타입을 구조체(structure)라고 합니다.

구조체는 사용자가 직접 타입을 만들기 때문에 type키워드를 사용하여 선언해야 합니다.

이전 시간에, const 목록을 이용한 enum 정의 부분에서, 아래와 같이 타입을 정의했던 것처럼

type ColorType int

const (
	Red ColorType = iota
	Blue
	Yellow
	Black
)

아래와 같은 형태로 구조체를 정의합니다.

type 구조체타입명 struct {
	필드명 데이터타입
}

구조체명과 필드명의 맨 첫번째 글자를 대문자로 정의할 경우, 패키지 외부로 공개(public)되는 타입입니다.

2.2. 기본 사용법

선언과 동시에 모든 필드값을 초기화시키고자할 때, 기본적으로 아래와 같이 필드명: 값 형태로 초기화 하며, 이렇게 필드명을 직접 설정할 때에는 일부 변수만 초기화 할 수 있습니다.

package main  

import "fmt"  

type Member struct {  
  Name string  
  Age  int  
}  

func main() {  
  m1 := Member{Name: "jay", Age: 38}  
  fmt.Println(m2)

  m2 := Member{Name: "jane"}  
  fmt.Println(m2)  
}
{jay 38}
{janae 0}

모든 필드를 초기화할 경우에는 필드명을 생략할 수 있고

m1 := Member{"jini", 32}  
fmt.Println(m1)

초기화시, 맨 마지막 }를 선언필드와 별도의 줄로 작성할 때에는 반드시 , 를 작성해야 합니다.

m3 := Member{  
  "rora",  
  29,  
}

타입을 먼저 선언하고 나중에 값을 설정할 수도 있습니다.

var m2 Member  
m2.name = "coco"  
m2.age = 22

2.3. 메모리 정렬

데이터에 효과적으로 접근하기 위해 메모리를 일정 크기 간격으로 정렬 하는 것

레지스터의 크기와 데이터의 크기가 같게 정렬되어있을 경우에 데이터를 효율적으로 읽어들일 수 있습니다.

레지스터는 실제 연산에 사용되는 데이터가 저장되는 곳으로, 레지스터 크기가 8byte인 컴퓨터를 64bit 컴퓨터라고 부릅니다.

package main  

import (  
  "fmt"  
  "unsafe")  

type Student struct {  
  Age   int32  
  Score float64  
}

func main() {  

  u1 := Student{Age: 32, Score: 9.5}  
  fmt.Println(unsafe.Sizeof(u1), &u1.Age, &u1.Score)
}
16 0x14000112010 0x14000112018

64bit 컴퓨터의 경우에는 각 데이터를 8의 배수에 담는 것이 효율적이기 때문에, 데이터 시작 주소는 8의 주소 배수에 위치합니다.

그리고, 구조체 내의 변수는 각 타입에 따라 타입의 byte수의 배수에 위치해 있습니다.
int32 타입은 4의 배수위치에, float64타입은 8의 배수 위치에 있습니다.

int8은 1의 배수, int16은 2의 배수


따라서, u1 데이터의 크기(unsafe.Sizeof) 결과가 16이 나옵니다.
이것을 그림으로 나타내면 아래와 같습니다.



이러한 성질을 토대로

int8타입의 필드 A, C, E와 int타입의 필드 B, D 로 구성된 User구조체가 있다고 할 때,
이 구조체의 필드 배치를 그대로 두는 것이 아니라
최적의 메모리 배치를 고려하여 패딩을 최소화하는 것이 좋습니다.

이렇게 생긴 구조체를

type User struct {  
  A int8  
  B int  
  C int8  
  D int  
  E int8  
}

이렇게 변경하면

type User struct {  
  A int8
  C int8
  E int8
  B int  
  D int
}

아래와 같이 메모리 낭비를 줄일 수 있습니다. (40 -> 24)



728x90
반응형

'Go' 카테고리의 다른 글

[Go] 09. 문자열  (0) 2024.01.18
[Go] 08. 포인터  (0) 2024.01.18
[Go] 06. if문, switch문, for문  (0) 2024.01.16
[Go] 05. 함수, 상수  (0) 2024.01.15
[Go] 03. fmt - 2. 표준입력  (0) 2024.01.14