10 minute read

개요

배열(array)은 고정 크기의 같은 타입 요소들의 연속된 모음입니다.

주요 특징:

  • 고정 크기: 선언 시 크기가 결정되며 변경 불가
  • 값 타입: 할당/전달 시 전체 복사 발생
  • 제로값 초기화: 선언 시 자동으로 제로값으로 초기화
  • 타입의 일부: 크기가 타입의 일부 ([3]int[5]int는 다른 타입)
  • 인덱스 접근: 0부터 시작하는 인덱스로 접근

배열 vs 슬라이스

// 배열: 고정 크기, 값 타입
var arr [3]int        // [3]int 타입

// 슬라이스: 가변 크기, 참조 타입
var slice []int       // []int 타입 (크기 명시 없음)

중요: 대부분의 경우 슬라이스를 사용하는 것이 권장됩니다. 배열은 크기가 고정되어 있어 유연성이 떨어집니다.

배열 선언 및 초기화

1. 기본 선언

package main

import "fmt"

func main() {
    // 크기 지정 선언 (제로값으로 초기화)
    var arr1 [5]int
    fmt.Println(arr1) // [0 0 0 0 0]
    
    // 다양한 타입의 배열
    var arr2 [3]string
    fmt.Println(arr2) // ["" "" ""]
    
    var arr3 [2]bool
    fmt.Println(arr3) // [false false]
    
    var arr4 [4]float64
    fmt.Println(arr4) // [0 0 0 0]
}

2. 초기화 방법

func initialization() {
    // 방법 1: 리터럴로 초기화
    arr1 := [3]int{1, 2, 3}
    fmt.Println(arr1) // [1 2 3]
    
    // 방법 2: 일부만 초기화 (나머지는 제로값)
    arr2 := [5]int{1, 2}
    fmt.Println(arr2) // [1 2 0 0 0]
    
    // 방법 3: 인덱스 지정 초기화
    arr3 := [5]int{0: 10, 2: 20, 4: 40}
    fmt.Println(arr3) // [10 0 20 0 40]
    
    // 방법 4: ... 으로 크기 자동 결정
    arr4 := [...]int{1, 2, 3, 4, 5}
    fmt.Println(arr4)        // [1 2 3 4 5]
    fmt.Println(len(arr4))   // 5
    
    // 방법 5: 혼합 초기화
    arr5 := [...]string{0: "first", 5: "sixth"}
    fmt.Println(arr5)        // [first     sixth]
    fmt.Println(len(arr5))   // 6
}

3. 배열 타입과 크기

func arrayTypes() {
    arr1 := [3]int{1, 2, 3}
    arr2 := [5]int{1, 2, 3, 4, 5}
    
    // 크기가 타입의 일부이므로 다른 타입
    fmt.Printf("arr1 type: %T\n", arr1) // [3]int
    fmt.Printf("arr2 type: %T\n", arr2) // [5]int
    
    // ❌ 다른 크기의 배열은 할당 불가
    // arr1 = arr2 // 컴파일 에러: cannot use arr2 (type [5]int) as type [3]int
}

배열 접근 및 수정

func accessAndModify() {
    arr := [5]int{10, 20, 30, 40, 50}
    
    // 인덱스로 접근
    fmt.Println(arr[0])  // 10
    fmt.Println(arr[4])  // 50
    
    // 값 수정
    arr[0] = 100
    arr[4] = 500
    fmt.Println(arr)     // [100 20 30 40 500]
    
    // 길이
    fmt.Println(len(arr)) // 5
    
    // ❌ 범위 초과 접근 시 패닉
    // fmt.Println(arr[5])  // panic: index out of range
    
    // ❌ 음수 인덱스 불가
    // fmt.Println(arr[-1]) // 컴파일 에러
}

배열 순회

1. for 루프

func iterateWithFor() {
    arr := [5]int{1, 2, 3, 4, 5}
    
    // 인덱스 사용
    for i := 0; i < len(arr); i++ {
        fmt.Printf("arr[%d] = %d\n", i, arr[i])
    }
}

2. range 루프

func iterateWithRange() {
    arr := [3]string{"Go", "Python", "Java"}
    
    // 인덱스와 값 모두 사용
    for index, value := range arr {
        fmt.Printf("[%d]: %s\n", index, value)
    }
    
    // 값만 사용
    for _, value := range arr {
        fmt.Println(value)
    }
    
    // 인덱스만 사용
    for index := range arr {
        fmt.Println(index)
    }
}

다차원 배열

1. 2차원 배열

func twoDimensionalArray() {
    // 2x3 배열
    var matrix [2][3]int
    fmt.Println(matrix) // [[0 0 0] [0 0 0]]
    
    // 초기화
    matrix2 := [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    fmt.Println(matrix2) // [[1 2 3] [4 5 6]]
    
    // 요소 접근
    fmt.Println(matrix2[0][0]) // 1
    fmt.Println(matrix2[1][2]) // 6
    
    // 수정
    matrix2[0][0] = 10
    fmt.Println(matrix2) // [[10 2 3] [4 5 6]]
}

2. 다차원 배열 순회

func iterateMultiDimensional() {
    matrix := [3][3]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    
    // 중첩 for 루프
    for i := 0; i < len(matrix); i++ {
        for j := 0; j < len(matrix[i]); j++ {
            fmt.Printf("%d ", matrix[i][j])
        }
        fmt.Println()
    }
    
    // range 사용
    for i, row := range matrix {
        for j, val := range row {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
        }
    }
}

3. 3차원 배열

func threeDimensionalArray() {
    // 2x3x4 배열
    var cube [2][3][4]int
    
    // 초기화
    cube[0][0][0] = 1
    cube[1][2][3] = 100
    
    fmt.Printf("Type: %T\n", cube)
    fmt.Printf("Length: %d\n", len(cube))
}

배열 복사 (값 타입)

func arrayCopy() {
    arr1 := [3]int{1, 2, 3}
    
    // 배열 할당은 전체 복사
    arr2 := arr1
    arr2[0] = 100
    
    fmt.Println(arr1) // [1 2 3] (변경 안 됨)
    fmt.Println(arr2) // [100 2 3]
    
    // 명시적 복사
    var arr3 [3]int
    copy(arr3[:], arr1[:]) // 슬라이스로 변환 후 복사
    fmt.Println(arr3)      // [1 2 3]
}

배열 비교

func arrayComparison() {
    arr1 := [3]int{1, 2, 3}
    arr2 := [3]int{1, 2, 3}
    arr3 := [3]int{1, 2, 4}
    
    // 같은 크기, 같은 타입의 배열은 비교 가능
    fmt.Println(arr1 == arr2) // true
    fmt.Println(arr1 == arr3) // false
    
    // 다른 크기는 비교 불가
    arr4 := [4]int{1, 2, 3, 4}
    // fmt.Println(arr1 == arr4) // 컴파일 에러
    
    // 슬라이스는 비교 불가 (== nil만 가능)
    slice1 := []int{1, 2, 3}
    slice2 := []int{1, 2, 3}
    // fmt.Println(slice1 == slice2) // 컴파일 에러
    fmt.Println(slice1 == nil) // false
}

함수 인자로 배열 전달

// 값 복사 전달 (비효율적)
func modifyArrayByValue(arr [5]int) {
    arr[0] = 999
    fmt.Println("Inside function:", arr)
}

// 포인터 전달 (효율적)
func modifyArrayByPointer(arr *[5]int) {
    arr[0] = 999
    fmt.Println("Inside function:", *arr)
}

// 슬라이스로 전달 (가장 일반적)
func modifySlice(slice []int) {
    slice[0] = 999
    fmt.Println("Inside function:", slice)
}

func main() {
    // 값 전달
    arr1 := [5]int{1, 2, 3, 4, 5}
    modifyArrayByValue(arr1)
    fmt.Println("After value:", arr1) // [1 2 3 4 5] (변경 안 됨)
    
    // 포인터 전달
    arr2 := [5]int{1, 2, 3, 4, 5}
    modifyArrayByPointer(&arr2)
    fmt.Println("After pointer:", arr2) // [999 2 3 4 5] (변경됨)
    
    // 슬라이스 전달
    slice := []int{1, 2, 3, 4, 5}
    modifySlice(slice)
    fmt.Println("After slice:", slice) // [999 2 3 4 5] (변경됨)
}

배열과 슬라이스 변환

func arrayToSlice() {
    arr := [5]int{1, 2, 3, 4, 5}
    
    // 배열 전체를 슬라이스로
    slice1 := arr[:]
    fmt.Printf("Type: %T, Value: %v\n", slice1, slice1)
    // Type: []int, Value: [1 2 3 4 5]
    
    // 부분 슬라이스
    slice2 := arr[1:4]
    fmt.Println(slice2) // [2 3 4]
    
    // 슬라이스 수정 시 원본 배열도 변경
    slice1[0] = 100
    fmt.Println(arr)    // [100 2 3 4 5]
    fmt.Println(slice1) // [100 2 3 4 5]
}

func sliceToArray() {
    slice := []int{1, 2, 3, 4, 5}
    
    // 슬라이스를 배열로 복사
    var arr [5]int
    copy(arr[:], slice)
    fmt.Println(arr) // [1 2 3 4 5]
    
    // 슬라이스 수정은 배열에 영향 없음
    slice[0] = 100
    fmt.Println(slice) // [100 2 3 4 5]
    fmt.Println(arr)   // [1 2 3 4 5]
}

실전 활용 패턴

1. 고정 크기 버퍼

type RingBuffer struct {
    data  [10]int
    index int
}

func (rb *RingBuffer) Add(value int) {
    rb.data[rb.index%10] = value
    rb.index++
}

func (rb *RingBuffer) GetAll() [10]int {
    return rb.data
}

func main() {
    rb := &RingBuffer{}
    for i := 0; i < 15; i++ {
        rb.Add(i)
    }
    fmt.Println(rb.GetAll()) // [10 11 12 13 14 5 6 7 8 9]
}

2. 조회 테이블

// 요일 이름
var weekdays = [7]string{
    "Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday",
}

func getDayName(day int) string {
    if day < 0 || day > 6 {
        return "Invalid"
    }
    return weekdays[day]
}

// 월 일수
var daysInMonth = [12]int{
    31, 28, 31, 30, 31, 30,
    31, 31, 30, 31, 30, 31,
}

func getDaysInMonth(month int) int {
    if month < 1 || month > 12 {
        return 0
    }
    return daysInMonth[month-1]
}

3. 행렬 연산

type Matrix [3][3]int

func (m *Matrix) Add(other Matrix) Matrix {
    var result Matrix
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            result[i][j] = m[i][j] + other[i][j]
        }
    }
    return result
}

func (m *Matrix) Multiply(scalar int) Matrix {
    var result Matrix
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            result[i][j] = m[i][j] * scalar
        }
    }
    return result
}

func main() {
    m1 := Matrix{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    
    m2 := Matrix{
        {9, 8, 7},
        {6, 5, 4},
        {3, 2, 1},
    }
    
    sum := m1.Add(m2)
    fmt.Println(sum) // [[10 10 10] [10 10 10] [10 10 10]]
    
    product := m1.Multiply(2)
    fmt.Println(product) // [[2 4 6] [8 10 12] [14 16 18]]
}

4. 고정 크기 스택

type Stack struct {
    data [100]int
    top  int
}

func (s *Stack) Push(value int) bool {
    if s.top >= 100 {
        return false // 스택 가득 찬 경우
    }
    s.data[s.top] = value
    s.top++
    return true
}

func (s *Stack) Pop() (int, bool) {
    if s.top == 0 {
        return 0, false // 스택 비어있는 경우
    }
    s.top--
    return s.data[s.top], true
}

func (s *Stack) Peek() (int, bool) {
    if s.top == 0 {
        return 0, false
    }
    return s.data[s.top-1], true
}

func (s *Stack) IsEmpty() bool {
    return s.top == 0
}

func main() {
    stack := &Stack{}
    stack.Push(10)
    stack.Push(20)
    stack.Push(30)
    
    if val, ok := stack.Pop(); ok {
        fmt.Println("Popped:", val) // 30
    }
    
    if val, ok := stack.Peek(); ok {
        fmt.Println("Top:", val) // 20
    }
}

5. 체스판/게임 보드

type ChessBoard [8][8]string

func NewChessBoard() ChessBoard {
    board := ChessBoard{}
    
    // 폰 배치
    for i := 0; i < 8; i++ {
        board[1][i] = "♙" // 흰색 폰
        board[6][i] = "♟" // 검은색 폰
    }
    
    // 다른 말 배치...
    board[0][0] = "♖" // 흰색 룩
    board[0][7] = "♖"
    
    return board
}

func (cb *ChessBoard) Display() {
    for i := 7; i >= 0; i-- {
        fmt.Printf("%d ", i+1)
        for j := 0; j < 8; j++ {
            if cb[i][j] == "" {
                fmt.Print("· ")
            } else {
                fmt.Printf("%s ", cb[i][j])
            }
        }
        fmt.Println()
    }
    fmt.Println("  a b c d e f g h")
}

6. 바이트 배열 처리

func byteArrayProcessing() {
    // IP 주소 (4바이트)
    ip := [4]byte{192, 168, 1, 1}
    fmt.Printf("IP: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3])
    
    // MAC 주소 (6바이트)
    mac := [6]byte{0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}
    fmt.Printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
    
    // UUID (16바이트)
    uuid := [16]byte{
        0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF,
        0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF,
    }
    fmt.Printf("UUID: %x\n", uuid)
}

성능 고려사항

1. 배열 vs 슬라이스

import "testing"

func BenchmarkArrayCopy(b *testing.B) {
    arr := [1000]int{}
    for i := 0; i < b.N; i++ {
        _ = arr // 전체 복사 (4KB)
    }
}

func BenchmarkSliceReference(b *testing.B) {
    slice := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        _ = slice // 포인터만 복사 (24바이트)
    }
}

// 결과: 슬라이스가 훨씬 빠름

2. 큰 배열은 포인터로 전달

type LargeArray [10000]int

// ❌ 비효율적: 40KB 복사
func processByValue(arr LargeArray) {
    // ...
}

// ✅ 효율적: 8바이트 포인터
func processByPointer(arr *LargeArray) {
    // ...
}

// ✅ 가장 권장: 슬라이스 사용
func processBySlice(slice []int) {
    // ...
}

3. 배열 크기와 스택

func stackVsHeap() {
    // 작은 배열: 스택 할당
    small := [10]int{}
    _ = small
    
    // 큰 배열: 힙으로 이스케이프될 수 있음
    large := [1000000]int{}
    _ = large
    
    // 슬라이스: 항상 힙 할당
    slice := make([]int, 1000000)
    _ = slice
}

일반적인 실수

1. 배열과 슬라이스 혼동

func confusion() {
    // ❌ 배열로 착각 (실제로는 슬라이스)
    slice := []int{1, 2, 3}
    
    // ✅ 배열
    array := [3]int{1, 2, 3}
    
    fmt.Printf("Slice type: %T\n", slice) // []int
    fmt.Printf("Array type: %T\n", array) // [3]int
}

2. 크기가 다른 배열 할당

func sizeMismatch() {
    arr1 := [3]int{1, 2, 3}
    arr2 := [5]int{1, 2, 3, 4, 5}
    
    // ❌ 컴파일 에러: 다른 타입
    // arr1 = arr2
    
    // ✅ 슬라이스로 변환 후 복사
    copy(arr1[:], arr2[:3])
    fmt.Println(arr1) // [1 2 3]
}

3. 범위 초과 접근

func outOfBounds() {
    arr := [3]int{1, 2, 3}
    
    // ❌ 런타임 패닉
    // fmt.Println(arr[3]) // panic: index out of range
    
    // ✅ 안전한 접근
    if len(arr) > 3 {
        fmt.Println(arr[3])
    }
}

4. 배열 함수 인자의 복사 비용 간과

type HugeArray [100000]int

// ❌ 매번 400KB 복사
func processHuge(arr HugeArray) {
    // ...
}

// ✅ 포인터 사용
func processHugePtr(arr *HugeArray) {
    // ...
}

// ✅✅ 슬라이스 사용
func processSlice(slice []int) {
    // ...
}

func main() {
    huge := HugeArray{}
    
    // processHuge(huge)      // 느림
    processHugePtr(&huge)     // 빠름
    processSlice(huge[:])     // 가장 권장
}

5. range에서 값 복사

type LargeStruct struct {
    data [1000]int
}

func rangeValueCopy() {
    arr := [10]LargeStruct{}
    
    // ❌ 각 반복마다 4KB 복사
    for _, item := range arr {
        _ = item
    }
    
    // ✅ 인덱스로 접근
    for i := range arr {
        _ = arr[i]
    }
    
    // ✅ 포인터 배열 사용
    ptrArr := [10]*LargeStruct{}
    for _, item := range ptrArr {
        _ = item // 포인터만 복사
    }
}

배열 사용 시기

배열을 사용해야 할 때

  1. 고정된 크기가 명확한 경우
    • IP 주소 [4]byte
    • RGB 색상 [3]uint8
    • 체스판 [8][8]string
  2. 값 의미론이 중요한 경우
    • 배열 할당/비교가 값 전체에 대해 동작
  3. 스택 할당이 필요한 경우
    • 작은 고정 크기 데이터

슬라이스를 사용해야 할 때 (대부분의 경우)

  1. 크기가 가변적인 경우
  2. 함수 인자로 전달할 경우
  3. 큰 데이터 구조의 경우
  4. append, copy 등 유틸리티 함수 사용 시

정리

  • 배열은 고정 크기의 값 타입 (크기가 타입의 일부)
  • 선언 시 자동으로 제로값 초기화
  • 배열 할당/전달 시 전체 복사 발생 (성능 주의)
  • 같은 타입/크기의 배열만 비교 가능 (==, !=)
  • 슬라이스는 배열의 참조를 감싼 구조체
  • 대부분의 경우 슬라이스 사용 권장
  • 고정 크기가 명확하고 작은 경우에만 배열 사용
  • 큰 배열은 포인터로 전달하거나 슬라이스로 변환
  • 다차원 배열: [rows][cols]type 형태
  • len() 함수로 배열 길이 확인
  • range로 순회 시 인덱스와 값 복사본 반환