포인터
Go에는 포인터가 존재한다. 포인터는 값의 메모리 주소를 저장한다.
값의 앞에 *를 붙임으로써 포인터를 정의하며, 포인터의 zero value는 nil이다.
&는 피연산자에 대한 포인터를 생성한다.
i := 42
p = &i // p는 i를 가리키는 포인터
* 는 포인터의 값을 나타낸다.
이를 역참조라고 부르는데 C언어와 달리 Go에는 포인터 연산이 없다.
간단한 예시 코드를 보자
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // p는 i를 가리킨다.
fmt.Println(*p) // p가 가리치고 있는 대상(i)의 값을 출력한다.
*p = 21 // 포인터를 이용해서 i 값을 21로 바꿔준다.
fmt.Println(i) // i의 값을 출력
p = &j // p가 j를 가리키도록 한다.
*p = *p / 37 // j의 값을 포인터를 이용해서 나누어준다.
fmt.Println(j) // j의 새로운 값을 출력해본다.
}
42
21
73
Struct
Go에서 struct는 Custom data type을 표한하는데 사용된다.
Go의 struct는 필드(field)들의 집합체이고 컨테이너이다.
특이한 점은 go의 구조체는 필드 값만 포함하며, 메서드를 갖지 않는다. 즉, Go에는 전통적인 객체 지향 언어(OOP)들이 가지는 클래스, 객체, 상속의 개념이 없다(!)
따라서 전통적인 OOP에서의 Class 개념은 Go에서 Struct로 표현된다.
Struct 선언방법
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
위의 예제 코드처럼 구조체를 선언하기 위해서는 custom type을 정의하는데 사용하는 type 문을 사용한다.
위의 경우 구조체의 이름이 대문자로 시작하므로 패키지의 외부에서 사용할 수 있다. vertex와 같이 소문자로 시작하는 이름을 가졌다면 외부에서 사용할 수 없게 된다.
Struct 객체 생성
선언된 구조체로부터 객체를 생성하는 방법은 몇 가지가 있다.
- 빈 객체 할당 후 필드값을 채워넣는 방법
- v := Vertex{}
v.X = 4
x.Y = 7 - 객체 생성 시 초기값을 함께 할당하는 방법위와 같이 필드 값을 순차적으로 넣을 수 있다. 또는 아래와 같이 필드명을 지정 후 값을 넣을 수 도 있다.생략된 필드들은 zero value를 갖게된다.
- v := Vertex{X: 7, Y: 3}
- v := Vertex{4, 7}
- Go의 내장함수 new() 사용하는 방법
- v := new(Vertex)
v.X = 3
new()를 사용해서 객체를 생성하면 모든 필드를 zero value로 초기화하고, 객체의 포인터를 반환한다.
이때에도 필드 값에 접근할 때 .(dot)을 이용하는데 이때 포인터는 자동으로 dereference된다. (C언어에서 포인터의 경우 -> 를 사용하는 문법과 다르다.
Struct와 포인터
Struct 필드들은 struct 포인터를 통해서 접근이 가능하다.
struct 포인터 p를 이용하여 필드 X에 접근하려면 (*p).X와 같이 하면 된다.
하지만 위의 표기법은 번거롭기 때문에 우리의 친절한 Golang은 그냥 p.X로 작성하는 것도 허용한다.
배열
Golang에서 배열의 선언은 [n]T의 형태로 한다.
- n 은 배열의 크기이고, T는 자료형이다.
ex) var a [10]int
당연한 이야기이지만 배열은 크기가 고정된 자료형이다. 그렇다면 크기가 동적인 데이터는 어디에 저장하는것이 좋을까? 이에 대한 한 가지 해결책을 알아보자.
슬라이스(Slice)
슬라이스 자료형은 내부적으로 배열에 기초해서 만들어졌으나 배열의 한계를 해결하여 더 편리하고 유용한 기능들을 제공한다.
일단, 고정된 사이즈가 아닌 동적인 사이즈를 지원한다.
따라서 초기 선언시 사이즈를 명시하지 않아도 되며 선언한 이후 언제든지 크기를 변경하거나 부분 배열을 추출해내는 것도 가능하다.
func main() {
primes := [6]int{2, 3, 5, 7, 11}
var s []int = primes[1:4]
fmt.Println(s)
}
[3 5 7]
슬라이스는 배열을 참조할 뿐 실제로 데이터를 저장하지는 않는다.
배열을 참조하고 있으므로 슬라이스의 원소를 변경하면 배열의 원소도 변경된다. 아래의 간단한 예시를 보자.
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
fmt.Println(names)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
[John Paul George Ringo]
[John Paul]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
배열을 리터럴로 선언할 때 위와 같이 줄바꿈 형식으로 선언한다면 마지막 원소 뒤에도 ,를 붙여주어야 한다. 안붙여주면 컴파일 에러가 발생한다.
Slicing low, high bound
파이썬에서 리스트 인덱싱과 똑같은 방식대로 인덱스의 low bound인 0과 high bound인 슬라이스의 길이 값은 생략할 수 있다.
a[0:10]
a[:10]
a[0:]
a[:]
Length와 Capacity
슬라이스 자료형에는 length와 capacity 값이 존재하는데 length는 말 그대로 포함하고 있는 원소의 갯수를 의미한다.
capacity는 슬라이스가 참조하고 있는 배열에 존재하는 원소의 개수를 나타내는 값인데, 슬라이스의 첫번째 원소가 위치한 곳부터 카운팅 한다.
각각 슬라이스 s에 대해 len(s), cap(s)와 같은 표현으로 값을 얻을 수 있다. 자세한 예제를 통해 알아보자.
Nil slices
슬라이스 자료형의 zero value는 nil이다. 이를 nil 슬라이스라고 지칭하는데 당연한 얘기지만 length 와 capacity 둘 다 0이며 참조하고 있는 배열 또한 없다.
make 활용해서 slice 생성하기
Go의 내장함수인 make 를 활용해서 슬라이스를 만들 수 있다. 이렇게 함으로써 동적인 크기의 배열을 만드는 것과 같은 효과를 낼 수 있다.
a := make([]int, 5)
make 함수는 0으로 값들이 초기화된 배열을 생성 하고 해당 배열을 참조하는 슬라이스를 반환한다.
만약 capacity 를 특정한 값으로 정해주고 싶다면 아래와 같이 3번째 argument를 전달해주면 된다.
b := make([]int, 0, 5) // len(b) = 0, cap(b) = 5
append로 슬라이스에 값 추가하기
Go의 내장 함수인 append 를 활용해서 슬라이스에 값을 추가할 수 있다.
func append(s []T, vs ...T) []T
- s : T 타입의 슬라이스
- 나머지 T 값들 : s슬라이스에 넣을 값들
append 는 원래의 슬라이스에 값을 추가한 슬라이스를 반환하는데 만약 슬라이스가 참조중인 배열의 크기가 작아서 값을 다 추가할 수 없다면 자동으로 크기가 더 큰 새로운 배열을 생성하고 해당 배열을 참조하게 된다.
Range
for 루프에서 range 를 사용하여 slice나 map 자료형에 대해 이터레이션을 수행할 수 있다.
이때 각 iteration마다 index와 해당 인덱스의 값을 반환한다.
var foo = []int{1, 2, 3, 4, 5, 6, 7}
func main() {
for i, v := range foo {
fmt.Printf("%d번째 인덱스의 값: %d\n", i, v)
}
}
Python에서와 비슷하게 _ 를 활용해서 index나 value 중 필요 없는 값은 스킵함으로써 메모리를 절약할 수 있다.
for _, v := range foo {
// something
}
맵(Map)
- Map은 key-value 형태로 값을 저장하는 자료형이다.
- 파이썬의 dictionary와 같은 기능
- make 함수로 생성 가능하다
type Person struct {
Name string
Age int
}
var m map[string]Person
func main() {
m = make(map[string]Person)
m["putapple96"] = Person{
"Subin", 27,
}
fmt.Println(m["putapple96"])
}
물론 아래와 같이 struct과 비슷하게 리터럴로 생성도 가능하다.
type Person struct {
Name string
Age int
}
var m = map[string]Person{
"putapple96" : Person{
"Subin", 27,
},
}
func main() {
fmt.Println(m)
}
map[putapple96:{Subin 27}]
값 추가, 삭제
- 추가 (해당 키 값이 이미 값을 가진 상태라면 업데이트)는 간단하다.
- m[key] = value
- 키에 해당되는 값을 구하는 것 역시 간단하다.
- value = m[key]
- 값 삭제는 내장함수 delete 를 활용한다.
- delete(m, key)
- 맵에 키 값이 존재하는지는 다음과 같이 확인한다.
- 맵 m에 key가 존재한다면 exist는 true 값을 반환 받으며 그렇지 않다면 false값을 반환 받는다.
- 또한 key 값이 존재하지 않으면 value는 m의 원소 타입의 zero value 값을 반환 받는다.
- value, exist = m[key]
연습문제 : 단어 개수 세기
문제)
문자열 s에 등장한 단어의 횟수를 저장하는 map을 리턴하도록 WordCount 함수를 작성하라
힌트 : strings.Fields 활용
package main
import (
"golang.org/x/tour/wc"
"strings"
)
func WordCount(s string) map[string]int {
m := make(map[string]int)
w := strings.Fields(s)
for _, value := range w{
m[value]++
}
return m
}
func main() {
wc.Test(WordCount)
}
Function values
함수 또한 값처럼 생각할 수 있다. 따라서 함수 자체를 매개변수로 넘기는 등의 코드또한 작성이 가능하다.
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.Println(compute(math.Pow))
}
위의 예제를 보면 compute라는 float64타입의 함수는 float64 타입의 함수 fn을 매개변수로 받는다
- hypot(5, 12) : (5의 제곱 + 12의 제곱)의 제곱근인 13
- compute(hypot) : compute가 hypot(3, 4)를 리턴하므로 (3의 제곱 + 4의 제곱)의 제곱근인 5
- compute(math.Pow) : compute가 math.Pow(3, 4)를 리턴하므로 3의 4승인 81
클로저(Closure)
Go에서 함수는 클로저로서 활용될 수 도 있다.
- 클로저 : 함수 외부 scope의 변수를 참조하는 function value. 클로저는 외부의 변수를 읽거나 쓸 수 있게 된다.
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
위의 예제에서 adder 함수는 int 형의 익명함수를 리턴해준다. Go언어에서 함수는 일급함수로서 다른 함수로부터 리턴되는 리턴값으로 사용될 수 있음을 바로 앞 절에서 배웠다.
그런데 위의 예제에서는 익명함수가 해당 함수의 밖에 있는 sum 을 참조하고 있다. 익명함수 자체에는 sum 이라는 로컬 변수가 없으므로 바로 외부 scope의 adder 함수 의 sum을 참조한다.
연습 문제 : 피보나치 클로저
package main
import "fmt"
func fibonacci() func() int {
f1, f2 := 0, 1
return func() int {
f := f1
f1, f2 = f2, f2+f
return f
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
'programming > Golang' 카테고리의 다른 글
[Golang] A Tour of Go 정리 - 흐름 제어 (0) | 2022.03.06 |
---|---|
[Golang] A Tour of Go 정리 - 1 (0) | 2022.02.27 |
Comment