[Go] array
개요
배열(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 // 포인터만 복사
}
}
배열 사용 시기
배열을 사용해야 할 때
- 고정된 크기가 명확한 경우
- IP 주소
[4]byte - RGB 색상
[3]uint8 - 체스판
[8][8]string
- IP 주소
- 값 의미론이 중요한 경우
- 배열 할당/비교가 값 전체에 대해 동작
- 스택 할당이 필요한 경우
- 작은 고정 크기 데이터
슬라이스를 사용해야 할 때 (대부분의 경우)
- 크기가 가변적인 경우
- 함수 인자로 전달할 경우
- 큰 데이터 구조의 경우
- append, copy 등 유틸리티 함수 사용 시
정리
- 배열은 고정 크기의 값 타입 (크기가 타입의 일부)
- 선언 시 자동으로 제로값 초기화
- 배열 할당/전달 시 전체 복사 발생 (성능 주의)
- 같은 타입/크기의 배열만 비교 가능 (
==,!=) - 슬라이스는 배열의 참조를 감싼 구조체
- 대부분의 경우 슬라이스 사용 권장
- 고정 크기가 명확하고 작은 경우에만 배열 사용
- 큰 배열은 포인터로 전달하거나 슬라이스로 변환
- 다차원 배열:
[rows][cols]type형태 len()함수로 배열 길이 확인- range로 순회 시 인덱스와 값 복사본 반환