GO 는 포인터를 지원하며 포인터는 값의 메모리 주소를 가진다.
T
타입은 T 값을 가리키는 포인터이다. 이것의 zero value 는 nil
이다.
var p *int
&
연산자는 이것의 피연산자에 대한 포인터를 생성한다.
i := 42
p = &i
*
연산자는 포인터가 가리키는 주소의 값을 나타낸다.
fmt.Println(*p) // 포인터 p를 통해 i 읽기
*p = 21 // 포인터 p를 통해 i 설정
이것은 역 참조 또는 간접 참조로 알려져 있다.
C 언어와 다르게, GO는 포인터 산술을 지원하지 않는다.
GO 는 포인터와 동일하게 구조체도 지원한다.
구조체는 필드의 집합체이며 다음과 같이 사용할 수 있다.
type Vertex struct {
X int
Y int
}
구초체의 필드는 .(dot)
연산자로 접근할 수 있다.
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X) // 결과 4
}
구조체 포인터 를 통해서도 구조체 필드를 접근할 수 있다.
(*p).X
로 작성하면, 구조체 포인터 p
에서 구조체의 X
필드에 접근할 수 있다.
그러나 위 표기법은 번거로울 수 있다. 따라서 이 언어는 역 참조를 명시할 필요 없이 p.X
로 작성할 수 있다.
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}
구조체 리터럴은 필드 값을 나열하여 새로 할당된 구조체 값을 나타낸다.
접두사 &
은 구조체 값으로 포인터를 반환한다.
var (
v1 = Vertex{1, 2} // 타입 Vertex 를 가짐
v2 = Vertex{X: 1} // Y:0 을 생략
v3 = Vertex{} // X:0 그리고 Y:0
p = &Vertex{1, 2} // 타입 *Vertex 를 가짐
)
[n]T
타입은 타입이 T 인 n 값들의 배열이다.
var a [10]int
위 선언은 변수 a 를 10개의 정수들의 배열로 선언한 것이다.
배열의 길이는 그 타입의 일부이며, 배열의 크기를 조절할 수 없다.
배열은 고정된 크기를 가진 반면, 슬라이스는 배열의 요소들을 동적인 크기로 유연하게 볼 수 있다.
실제로, 슬라이스는 배열보다 훨씬 흔하다.
[ ]T
타입은 T 타입을 원소로 가지는 슬라이스이다.
슬라이스는 콜론으로 구분된 두 개의 인덱스(하한과 상한)를 지정하여 형성된다.
a [low : high]
이것은 첫 번째 요소를 포함하지만 마지막 요소를 제외하는 범위를 선택한다.
아래 표현은 a 의 인덱스 1 부터 네 번째 요소를 포함하는 슬라이스를 생성한다.
a [1 : 4]
슬라이스는 어떠한 데이터도 저장할 수 없으며 단지 배열의 한 영역을 나타낼 뿐이다.
슬라이스의 요소를 변경하면 배열의 해당 요소가 수정된다.
아래 예시는 배열을 공유하는 다른 슬라이스는 이런 변경사항을 확인할 수 있다.
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
실행 결과
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
슬라이스 리터럴은 길이가 없는 배열 리터럴과 같다.
아래는 배열 리터럴이다.
[3] bool {true, true, false}
이렇게 하면 위와 동일한 배열이 생성되고, 이를 참조하는 슬라이스가 만들어진다.
[] bool {true, true, false}
상한 또는 하한을 생략하면 슬라이싱 시 기본 값을 사용할 수 있다.
하한의 경우 기본값 0, 상한의 경우 슬라이스의 길이이다.
var a [10] int
위 배열에서 아래 슬라이스 표현식은 모두 동일하다.
a[0:10]
a[:10]
a[0:]
a[:]
슬라이스는 _length (길이)_
와 _capacity (용량)_
를 둘다 가지고 있다.
슬라이스의 길이는 슬라이스가 포함한 요소의 개수, 용량은 첫 번째부터 계산하는 요소의 개수이다.
슬라이스 s 의 길이와 용량은 len(s)
와 cap(s)
식으로 얻을 수 있다.
슬라이스 길이 연장은 충분한 용량이 있다면 다시 슬라이싱 하면 된다.
func main() {
s := []int{1, 2, 3, 4, 5}
printSlice(s)
s = s[:0]
printSlice(s)
s = s[:4]
printSlice(s)
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len = %d, cap = %d %v\n", len(s), cap(s), s)
}
위 결과는 다음과 같다.
len = 5, cap = 5 [1 2 3 4 5]
len = 0, cap = 5 []
len = 4, cap = 5 [1 2 3 4]
len = 2, cap = 3 [3 4]
슬라이스의 zero value 는 nil
이다.
nil 슬라이스의 길이와 용량은 0 이며, 기본 배열을 가지고 있지 않다.
내장 함수 make
로 슬라이스를 생성할 수 있다.
make 함수는 0 으로 이루어진 배열을 할당한다. 그리고 배열을 참조하는 슬라이스를 반환한다.
a := make([]int, 5) // len(a) = 5
용량을 지정하려면, make 함수의 세 번째 인자에 값을 전달한다.
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
슬라이스는 다른 슬라이스를 포함하여 모든 타입을 담을 수 있다.
func main() {
// Create a tic-tac-toe board.
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// The players take turns.
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}
위 결과는 다음과 같다.
X _ X
O _ X
_ _ O
GO 는 내장된 append
함수를 제공한다.
func append(s []T, vs ...T) []T
append 의 첫 번째 파라미터는 s 는 슬라이스의 타입 T 이다.
그리고 나머지 T 값들을 슬라이스에 추가할 값들이다.
append 의 결과 값은 원래 슬라이스의 모든 요소와 추가로 제공된 값들을 포함하는 슬라이스이다.
만약 s 의 원래 배열이 너무 작아서 주어진 값을 모두 추가할 수 없을 경우, 더 큰 배열이 할당된다.
이때 반환된 슬라이스는 새로 할당된 배열을 가리킨다.
for 에서 range
는 슬라이스 또는 맵의 요소들을 순회한다.
슬라이스에서 range 를 사용하면, 각 순회마다 두 개의 값이 반환된다.
첫 번째는 인덱스, 두 번째는 인덱스 값의 복사본이다.
다음과 같이 사용할 수 있다.
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
_
을 할당하여 인덱스 또는 값을 건너뛸 수 있다.
for i, _ := range pow
for _, value := range pow
만약 인덱스만을 원하면, 두 번째 변수를 생략할 수 있다.
for i := range pow
맵은 키를 값에 매핑한다.
맵의 zero value 는 nil
이며, nil 맵은 키도 없고, 키를 추가할 수도 없다.
make 함수는 주어진 타입의 초기화되고 사용 준비가 된 맵을 반환한다.
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
위 결과
{40.68433 -74.39967}
맵 리터럴은 구조체 리터럴과 같지만 키가 필요하다.
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
최상위 타입이 타입 이름일 경우 리터럴 요소에서 생략할 수 있다.
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
m 맵에 요소를 추가하거나 업데이트 하기
m[key] = elem
요소 검색하기
elem = m[key]
요소 제거하기
delete(m, key)
두 개의 값을 할당하여 키가 존재하는지 테스트
elem, ok = m[key]
위 코드에서 key
가 m
안에 있다면, ok
는 true
아니면 false
만약 key
가 맵 안에 없다면, elem
은 map 요소 타입의 zero value 이다.
실제 값에 상관없이 map 내부에 존재 여부만 필요할 경우, 공백 식별자 를 이용하여 변수가 있을 자리에 놓으면 된다.
_, ok = m[key]
함수들도 값이며, 다른 값들과 마찬가지로 전달될 수 있다.
함수 값은 함수의 인수나 반환 값으로 사용될 수 있다.
package main
import (
"fmt"
"math"
)
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Print(compute(math.Pow))
}
GO 함수들은 클로저일 수 있다.
클로저는 함수의 외부로부터 오는 변수를 참조하는 함수값이다.
함수는 참조된 변수에 접근하여 할당될 수 있다.
이런 의미에서 함수는 변수에 바운드 된다.
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos := adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i) // sum 이 유지된 상태로 계속 더해짐
)
}
}
각 클로저는 그 자체의 sum 변수에 바운드되어 있다.
위 adder 함수는 익명함수 func() int
를 리턴하는 함수이다.
그리고 이 익명함수는 바깥의 sum 변수를 바운드하고 있는데, 익명함수 자체가 로컬 변수 sum 이 아니기 때문에 외부 변수 sum 이 값이 증가되는 것을 유지되고 있다.
클로저를 사용하면 위의 변수 pos
로 함수를 호출할 때마다 계속 가져다 쓸 수 있다.
즉, 함수가 선언될 때의 환경을 계속 유지한다.
내장 함수로 메모리를 할당하지만 다른 언어에 존재하는 같은 이름의 기능과는 다르게 메모리를 초기화하지 않고, 단지 값을 제로화(zero) 한다.
다시 말해, new(T)
은 새로 제로값이 할당된 타입 T 를 가리키는 포인터를 반환하는 것이다.
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
때론 제로값만으로 충분하지 않고 생성자(constructor)
로 초기화할 필요가 있다.
다음과 같은 예제를 확인하자.
func newRect(width, height int) *Rect {
if width < 0 || height < 0 {
return nil
}
r := new(Rect)
r.width = width
r.height = height
return r
}
이 예제에는 불필요하게 반복되는 코드가 있다.
합성 리터럴 (앞서 배운 구조체 리터럴) 로 간소화하여 새로운 인스턴스를 만들어보자.
func newRect(width, height int) *Rect {
if width < 0 || height < 0 {
return nil
}
r := Rect{width, height}
return r
}
합성 리터럴의 필드들은 순서대로 배열되고 반드시 입력해야 한다.
하지만, 요소들에 레이블을 붙여 필드 : 값
형태로 짝을 만들면,
초기화 순서에 관계 없이 나타날 수 있다.
입력되지 않은 요소들은 각자의 제로값을 가진다.
제한적으로, 합성 리터럴이 전혀 필드가 없을 경우에는
new(Type)
은 &Type{ }
과 동일하다.
내장 함수 append 를 이용하면 슬라이스의 끝에 요소를 붙이고 결과를 반환할 수 있다.
형식은 다음과 같다.
func append(slice []T, elements ... T) []T
다음의 예제를 살펴보자.
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
위 결과는 [1 2 3 4 5 6] 을 출력한다.
그런데 append 를 이용하여 slice 에 slice 를 붙이고 싶다면 어떻게 해야할까?
쉬운 방법으로는 ...
를 이용하는 것이다.
아래 예제는 위와 동일한 결과를 산출한다.
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
...
이 없다면 컴파일되지 않는다 왜냐면 y 는 int 타입이 아니라 틀리기 때문이다.