var idMap map[int]string
Pointer (포인터)
포인터는 변수나 데이터 구조의 메모리 주소를 저장하는 변수.
일반적으로 변수는 값을 저장하는데 사용되지만, 포인터 변수는 값 자체를 저장하는 게 아니라,
메모리 주소를 저장하여 해당 메모리 위치에 접근하고 조작할 수 있게 함.
즉, 포인터 변수는 다른 변수의 메모리 주소를 가리키고 그 변수의 값을 간접적으로 참조하거나 변경할 수 있게 해줌.
* 타입 은 타입 값을 가리키는 포인터 변수를 만들 수 잇음
아래 선언만 했을 땐 nil 값을 가짐
var p *int
& 연산자도 포인터 변수로 만들 수 있음
왜냐? 해당 변수의 메모리 주소를 넣어줄 수 있기 떄문!
package main
import "fmt"
func main() {
i, j := 42, 2701
// var p *int = &i
// 자료형 앞에 *을 붙이면 포인터형 변수
// &i는 i의 메모리 주소를 나타냄
// 따라서 p는 i의 메모리 주소를 가리키는 포인터 변수가 됨
// p를 이용하여 i의 값을 가져오거나 변경 가능
// *p : 역참조 (포인터가 가리키는 메모리 위치의 값 가져옴)
// *p = 10 은 p가 가리키는 메모리 위치에 값을 저장 가능
p := &i // point to i
// *포인터_변수명 : 포인터형 변수에서 값 가져오기
fmt.Println(*p) // read i through the pointer
// p가 가리키는 메모리 위치에 21을 저장 가능
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
// p = &j 를 하면 p가 가리키는 메모리 위치가 2701임
// 따라서 *p는 2701임
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
cf. 역참조란?
역참조라는 거는 해당 메모리가 가리키는 값을 가져오는 것이다.
따라서, 위의 예시에서 *p = 21 을 입력하면, p가 가지는 메모리 주소가 가리키는 변수의 값을 가져오게 되고, 변수 값을 바꾼다는 것이다.
즉, 만약 해당 값을 변경한다면, 메모리 위치가 가리키는 값 자체가 변경된다.
(메모리 주소는 그대로이지만, 그 메모리 주소가 가리키는 값이 달라진다는 것)
해당 값을 바꾸게 되면 그 메모리가 가리키는 값 자체가 달라진다는 거네?
cf. C언어와는 다르게, Go는 포인터 산술을 지원하지 않는다.
C 언어에서는 포인터 산술을 사용하여 배열의 요소에 접근하거나 메모리 블록을 순회하는 등의 작업을 수행할 수 있다.
예를 들어, ptr + 1은 포인터 ptr이 가리키는 요소 다음에 위치한 요소의 주소를 나타낸다. 이를 통해 배열의 인덱스를 증가시키는 등의 작업을 쉽게 수행할 수 있다.
그러나 Go 언어에서는 포인터 산술이 허용되지 않는다. 이는 Go 언어가 메모리 안전성을 강조하는 설계 원칙 중 하나이다.
Go 언어에서는 안전한 메모리 관리를 위해 포인터 산술을 제한하고, 슬라이스(Slice)와 같은 추상화 개념을 제공하여 배열과 메모리 접근을 간소화하고 안전하게 처리할 수 있도록 지원한다.
따라서, 정리하면 GO 언어에서는 포인터를 사용하여 변수의 주소를 저장하고, 이를 통해 간접적으로 변수에 접근하거나 변경하는 등의 작업은 가능하지만, 포인터 산술을 사용하여 포인터 값을 조작하는 것은 허용되지 않는다.
Struct (구조체)
Go에서 struct는 Custom Data Type을 표현하는데 사용됨.
필드들의 집합체이며 필드들의 컨테이너이다. struct는 필드 데이타만을 가지며, (행위를 표현하는) 메서드를 갖지 않는다.
Go 언어는 객체지향 프로그래밍(Object Oriented Programming, OOP)을 고유의 방식으로 지원한다. 즉, Go에는 전통적인 OOP 언어가 가지는 클래스, 객체, 상속 개념이 없다. 전통적인 OOP의 클래스(class)는 Go 언어에서 Custom 타입을 정의하는 struct로 표현되는데, 전통적인 OOP의 클래스가 필드와 메서드를 함께 갖는 것과 달리 Go 언어의 struct는 필드만을 가지며, 메서드는 별도로 분리하여 정의한다.
Struct 선언
type 문 사용
만약 이 person 구조체를 패키지 외부에서 사용할 수 있게 하려면 (Go 패키지에서 설명하였듯이) struct명을 Person으로 변경하면 된다.
package main
import "fmt"
// struct 정의
type person struct {
name string
age int
}
func main() {
// person 객체 생성
p := person{}
// 필드값 설정
p.name = "Lee"
p.age = 10
fmt.Println(p)
}
외부 패키지에서 Struct 객체 생성 후 사용하는 예제 코드
패키지 노출 규칙에 의해 Go 언어에서 식별자의 첫 글자가 대문자로 시작하면 해당 식별자는 외부 패키지에서 접근 가능한 노출된(exported) 식별자가 된다.
type person struct를 type Person struct로 변경하면 첫 글자가 대문자로 시작하므로
Person은 패키지 외부에서 접근 가능한 노출된 식별자가 된다.
package main
import (
"fmt"
"yourpackage" // 외부 패키지 import
)
func main() {
p := yourpackage.Person{} // 외부 패키지의 Person 구조체 사용
p.Name = "Lee" // 필드값 설정
p.Age = 10
fmt.Println(p)
}
Struct 객체 생성
person{} 를 사용하여 빈 person 객체를 먼저 할당하고, 나중에 그 필드값을 채워넣는 방법이 있다.
struct 필드를 엑세스하기 위해서는 . (dot)을 사용한다.
struct 객체를 생성할 때, 초기값을 함께 할당하는 방법도 있다.
즉, 아래 첫번째 예처럼, struct 필드값을 순서적으로 { } 괄호안에 넣을 수 있으며, 두번째 예처럼 순서에 상관없이 필드명을 지정하고(named field) 그 값을 넣을 수 도 있다. 특히 두번째 예처럼 필드명을 지정하는 경우, 만약 일부 필드가 생략될 경우 생략된 필드들은 Zero value (정수인 경우 0, float인 경우 0.0, string인 경우 "", 포인터인 경우 nil 등)를 갖는다.
var p1 person
p1 = person{"Bob", 20}
p2 := person{name: "Sean", age: 50}
위의 코드에서 p1 변수만 = 이고 두 번째 변수인 p2가 := 인 이유는 p1 변수의 경우는 이미 선언되어 있지만, p2의 경우는 이미 선언되어 있지 않기 때문이다. 따라서 선언과 동시에 초기화를 진행하려면 := 를 통해 짧은 변수 선언(Short variable declaration) 구문을 활용할 수 있다.
또 다른 객체 생성 방법으로 Go 내장함수 new()를 쓸 수 있다.
다만, new()를 사용해서 초기화하면 해당 변수가 포인터 변수가 된다.
왜냐??? new()를 사용하면 해당 객체의 메모리 주소가 리턴되기 때문!!!
new()를 사용하면 모든 필드를 Zero value로 초기화하고 person 객체의 포인터(*person)를 리턴한다.
객체 포인터인 경우에도 필드 엑세스 시 . (dot)을 사용하는데,
이 때 포인터는 자동으로 Dereference - 역참조 된다.
p := new(person)
p.name = "Lee" // p가 포인터라도 . 을 사용한다
Pass By Value vs. Pass By Reference
pass by value는 값을 복사하여 전달하며 원본 값을 변경하지 않음.
반면 pass by reference는 메모리 주소를 전달하여 값을 직접 참조하고 변경함.
Pass By Value (값에 의한 전달)
값을 복사하여 함수에 전달하는 방식.
함수 내부에서 전달된 값에 대한 변경이 있어도 원본 값에는 영향을 주지 않는다.
func updateValue(x int) {
x = x + 1
}
func main() {
num := 10
updateValue(num)
fmt.Println(num) // 출력: 10 (변경되지 않음)
}
Pass By Reference (참조에 의한 전달)
값의 메모리 주소를 전달하여 함수에서 해당 메모리를 직접 참조하는 방식.
함수 내부에서 전달된 값에 대한 변경이 있으면 원본 값도 변경된다.
package main
import "fmt"
func updateValue(x *int) { // 메모리 주소가 전달되므로 *int (포인터 변수)
*x = *x + 1
}
func main() {
num := 10
updateValue(&num) // 메모리 주소 전달 -> 값 직접 참조 및 변경 가능
fmt.Println(num) // 출력: 11 (변경됨)
}
updateValue(*num) 과 *num의 차이
다만 헷갈릴 수 있는 케이스가 있다.
바로 역참조 변수를 함수의 매개변수로 전달할 때와 그냥 역참조 변수에 값을 초기화 할 때이다.
매개변수로 전달할 때는 "값의 복사"가 일어나기 때문에 원본 변수가 영향을 받지 않는다.
그러나 그냥 역참조 변수에 값을 초기화 해줄 때는 원본 변수도 영향을 받는다.
"값의 복사" 없이 해당 포인터가 가리키는 주소의 값 자체가 달라지기 때문이다.
package main
import "fmt"
func updateValue(x int) { // 메모리 주소가 전달되므로 *int (포인터 변수)
x = x + 1
}
func main() {
// 역참조 변수를 함수의 매개변수로 전달 - "값의 복사"가 일어남
i := 10
num := &i
updateValue(*num)
fmt.Println(*num) // 출력: 10 (변경 안됨)
fmt.Println(i) // 10
// 역참조 변수에 다른 값 초기화 - 메모리 주소가 가리키는 값 달라짐
j := 28
p := &j
*p = 21 // j가 가리키는 메모리 위치에 21 저장
fmt.Println(j) // 21
}
Arrays
배열의 길이는 그 타입의 일부이므로 배열의 크기를 조정할 수 없다.'
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1]) // Hello World
fmt.Println(a) // [Hello World]
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes) // [2 3 5 7 11 13]
var b [3] int
b[0] = 1000
fmt.Println(b) // [1000 0 0]
fmt.Println(b[0]) // 1000
c:= [2]int{1} // 하나만 초기화 해도 된다
fmt.Println(c) // [1 0] - 초기화 안 한 것은 default값이 set
}
Slices
배열은 고정된 크기를 가지고 있다.
반면에, 슬라이스는 배열의 요소들을 동적인 크기로, 유연하다.
실제로, 슬라이스는 배열보다 훨씬 흔하게 쓰인다.
cf.
like.. 자바의 ArrayList ?!
Java에서 ArrayList는 동적으로 크기가 조정될 수 있는 배열이다.
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
len(), cap()
슬라이스의 길이와 용량을 잘 구별해서 알아둬야 한다.
길이 같은 경우는 실제 슬라이스에 저장된 요소의 개수를 말한다.
용량은 슬라이스의 길이가 용량을 초과할 때 용량만큼 새로운 요소를 저장할 수 있는 크기를 늘린
새 배열을 만들어주고 다시 슬라이스에 할당하는 역할을 한다.
s := make([]int, 1, 3)
s[0] = 10
s = append(s, 20) // 두 번째 요소 추가
fmt.Println(s) // [10, 20]
fmt.Println(len(s)) // 2 (길이가 2로 늘어남)
fmt.Println(cap(s)) // 3 (용량은 변화 없음)
즉, 데이터가 append로 추가되었을 때 슬라이스 용량(capacity)이 아직 남아 있는 경우는
그 용량 내에서 슬라이스의 길이(length)를 변경하여 데이타를 추가하고,
용량(capacity)을 초과하는 경우
현재 용량의 2배에 해당하는 새로운 Underlying array를 생성하고
기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당한다.
func main() {
s := make([]int, 5, 10)
println(len(s), cap(s)) // len 5, cap 10
}
참고로, 만약 따로 make 함수를 이용해서 길이와 용량을 넣어주지 않았을 경우엔
초기화 한 값의 길이와 용량이 동일하게 초기화 된다.
package main
import "fmt"
func main() {
s := []int{0, 1}
fmt.Println(len(s), cap(s)) // 2 2
}
그리고 부분 슬라이스를 하면 원본 슬라이스의 값이 바뀐다.
s := []int{0, 1, 2, 3, 4, 5}
s = s[2:5] // 2, 3, 4
s = s[1:] // 3, 4 - 현재 원본의 데이터가 sub-slice로 변경되어 2, 3, 4 이다.
fmt.Println(s) // 3, 4 출력
copy
값에 의한 전달, 즉 pass by value가 일어나서 copy한 변수에 변경이 있어도 원본 변수엔 변경이 없다.
package main
import "fmt"
func main() {
source := []int{0, 1, 2}
target := make([]int, len(source), cap(source)*2)
copy(target, source)
fmt.Println(target) // [0 1 2 ] 출력
println(len(target), cap(target)) // 3, 6 출력
source[0] = 20
fmt.Println(source, target) // [20 1 2] [0 1 2]
}
Slice 내부 구조
슬라이스는 내부적으로 사용하는 배열의 부분 영역인 세그먼트에 대한 메타 정보를 가지고 있다. 슬라이스는 크게 3개의 필드로 구성되어 있는데, 첫째 필드는 내부적으로 사용하는 배열에 대한 포인터 정보이고, 두번째는 세그먼트의 길이를, 그리고 마지막으로 세번째는 세그먼트의 최대 용량(Capacity)이다.
처음 슬라이스가 생성될 때, 만약 길이와 용량이 지정되었다면, 내부적으로 용량(Capacity)만큼의 배열을 생성하고, 슬라이스 첫번째 필드에 그 배열의 처음 메모리 위치를 지정한다. 그리고, 두번째 길이 필드는 지정된 (첫 배열요소로부터의) 길이를 갖게되고, 세번째 용량 필드는 전체 배열의 크기를 갖는다.
예를 들어, 아래 첫번 문장을 보면, 0부터 5까지 6개의 요소를 갖는 슬라이스를 생성하고 있음을 볼 수 있다. 이때, 슬라이스의 배열포인터는 내부 배열의 첫번째 요소인 0을 가리키고, 길이는 전체 6, 그리고 용량도 6으로 설정된다.
그런데, 만약 이 슬라이스 S 로부터 Sub-slice S[2:5]를 하게 되면 슬라이스의 내부데이타 어떻게 변경될까? S[2:5]는 인덱스2부터 인덱스4까지의 배열요소를 가리키므로, 슬라이스 S의 배열포인터는 세번째 배열요소인 2를 가리키고, 길이는 3을, 그리고 용량은 세번째 배열요소부터 배열 마지막까지 즉 4를 갖게된다.
Ref.
http://golang.site/go/article/13-Go-%EC%BB%AC%EB%A0%89%EC%85%98---Slice
예제로 배우는 Go 프로그래밍 - Go 컬렉션 - Slice
1. 슬라이스(Slice) Go 배열은 고정된 배열크기 안에 동일한 타입의 데이타를 연속적으로 저장하지만, 배열의 크기를 동적으로 증가시키거나 부분 배열을 발췌하는 등의 기능을 가지고 있지 않다.
golang.site
Slices are like references to arrays
slice 변수는 배열에 대한 참조 변수이다. (포인터 변수 같은 것)
즉, slice는 배열을 참조하기 때문에 slice를 통해 배열의 요소를 변경하면 원본 배열에 영향을 줄 수 있다.
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2] // names[0:2] 를 참조
b := names[1:3] // names[1:3] 을 참조
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b) // [John XXX] [XXX George]
fmt.Println(names) // [John XXX George Ringo]
// 원본 배열에도 영향이 감!
}
Maps
Map은 키(Key)에 대응하는 값(Value)을 신속히 찾는 해시테이블(Hash table)을 구현한 자료구조이다.
Go 언어는 Map 타입을 내장하고 있는데, "map[Key타입]Value타입" 과 같이 선언할 수 있다.
이때 선언된 변수 idMap은 (map은 reference 타입이므로) nil 값을 갖으며, 이를 Nil Map이라 부른다.
Nil map에는 어떤 데이타를 쓸 수 없는데, map을 초기화하기 위해 make()함수를 사용할 수 있다.
var idMap map[int]string
cf. Nil Map이란?
Nil 맵은 메모리에 할당되지 않은 상태를 의미합니다.
즉, 맵이 초기화되지 않았거나 값을 할당하지 않은 상태입니다.
Nil 맵은 nil 값으로 설정되어 있으며, 사용하기 전에 반드시 초기화해야 합니다.
Nil 맵은 길이나 용량을 가지지 않으며, 맵에 대한 기본적인 동작(요소 추가, 읽기, 삭제 등)을 수행할 수 없습니다.
Nil 맵을 사용하여 값을 읽으려고 하면 런타임 에러가 발생합니다.
한편, 초기화된 맵 상태란?
초기화된 맵은 make 함수나 리터럴을 사용하여 메모리에 할당되고 초기화된 상태를 의미합니다.
초기화된 맵은 실제 메모리 공간을 가지며, 맵의 길이와 용량이 설정됩니다.
초기화된 맵은 맵의 기본 동작을 수행할 수 있습니다.
값을 추가하거나 읽어올 수 있으며, 필요에 따라 요소를 삭제하거나 수정할 수도 있습니다.
정리하면, NilMap은 아직 맵이 메모리에 할당되지 않고 초기화되지 않은 상태를 말한다.
그러나 make() 함수는 해시 테이블 자료구조를 메모리에 생성하고 그 메모리를 가리키는 map value를 리턴한다.
즉, 그 메모리를 가리키는 맵 변수의 주소값을 반환하는 것이다.
따라서 맵 변수는 실제 데이터가 저장된 메모리 공간을 참조하고, 해당 주소를 통해 맵에 접근하고 조작할 수 있다.
그러나 왜 Nil 맵 상태의 변수를 등록할까?
어차피 맵이 메모리에 초기화되지 않아 아예 호출하거나 읽기 및 삭제 등을 못 사용하는데?!
그 효용은 무엇일까??
1. 초기화 지연:
- 맵을 사용해야 하지만 초기 값이 아직 필요하지 않을 때, 변수를 Nil 맵으로 선언하여 초기화를 지연시킵니다.
- 초기화를 나중에 수행하고자 할 때, Nil 맵으로 변수를 선언하여 필요한 시점에 초기화 코드를 실행할 수 있습니다.
- 초기화 지연을 통해 초기 값을 계산하거나 가져오는 작업을 늦출 수 있으며, 필요한 경우에만 초기화를 수행합니다.
2. 예외 처리:
- Nil 맵을 사용하여 예외 상황을 표현할 수 있습니다.
- 특정 조건이 충족되지 않은 경우에 Nil 맵을 반환하거나 전달함으로써 예외 상황을 나타낼 수 있습니다.
- 이를 통해 호출자에게 맵이 유효하지 않음을 알릴 수 있고, 필요한 추가 처리를 수행할 수 있습니다.
make() 함수를 사용해야만 해시 테이블 자료구조를 메모리에 생성하고 그 메모리를 가리키는 map value를 리턴한다.
var idMap map[int]string // nilMap 선언
idMap = make(map[int]string) // make 함수를 이용한 초기화
// 초기화 된 이후에 맵에 값을 할당하고 읽어올 수 있음
한번에 선언 및 초기화를 하고 싶다면 := 를 이용하여 make() 함수를 써주면 된다.
package main
import "fmt"
func main() {
m := make(map[string]int) // 선언과 초기화를 동시에
fmt.Println(m)
m["answer"] = 42
fmt.Println("The Value:", m["answer"])
m["answer"] = 48
fmt.Println("The value:", m["answer"])
delete(m, "answer")
fmt.Println("The value:", m["answer"])
v, ok := m["answer"]
fmt.Println("The value:", v, "Present?", ok)
}