Day 4 with Go-Lang
Go 컬렉션 - Map
Map 은 Key 에 대응하는 Value 를 빠르게 찾을 수 있는 Hash Table 을 구현한 자료구조이다. Go 언어는 Map 타입을 내장하고 있는데, map[KeyT]ValueT
의 형태로 선언할 수 있다.
예를 들어, 정수를 Key 로 갖고 문자열을 Value 로 갖는 맵 변수는 다음과 같이 선언할 수 있다.
var idMap map[int]string
이 때 선언된 변수 idMap 은 nil 값을 갖으며, 이를 Nil Map 이라 부른다. Nil Map 에는 데이터를 쓸 수 없고, Map 을 초기화하기 위해 make() 함수를 사용해야 한다. (Map 리터럴을 사용할 수도 있다.)
idMap = make(map[int]string)
make() 함수의 Parameter 로 map 키워드와 [Key Type] Value Type 을 지정하는데, 이때 make() 함수는 Hash Table 자료구조를 메모리에 생성하고, 그 메모리를 가리키는 Map value 를 Return 한다.
Map 은 make() 함수를 사용해 초기화할 수도 있지만, Literal 을 사용해 초기화할 수도 있다. Literal Initialization 은 다음과 같이 map[Key Type]Value Type {key : value, ...}
Map 타입 뒤의 {}
안에 “키 : 값” 들을 열거하면 된다.
// Literal Initialization
tickers := map[string]string {
"GOOG": "Google Inc",
"MSFT": "Microsoft",
"FB": "FaceBook",
}
- Map 의 사용
처음 Map 이 make() 함수에 의해 초기화 되었을 때는 아무 데이터가 저장되지 않은 상태이다. 이 때, 새로운 데이터를 추가하기 위해서 mapVariable[Key] = Value
와 같이 해당 키에 그 값을 할당하면 된다. 만약 기존의 키 값이 이미 존재했다면, 추가하지 않고 값만 갱신하게 된다.
package main
import "fmt"
func main() {
var m map[int]string = make(map[int]string)
m[901] = "Apple"
m[134] = "Grape"
m[777] = "Tomato"
fmt.Println(m)
str := m[134]
println(str)
noData := m[999]
println(noData)
delete(m, 777)
m[901] = "Test"
fmt.Println(m)
}
map 에서 특정 Key 의 Value 를 읽을 때는 위와 같이 m[Key]
를 읽으면 된다. 만약 Map 안에 찾는 Key 가 존재하지 않는 경우에 Reference 타입인 경우 nil 을, Value 타입인 경우 zero 를 Return 한다.
Map 에서 특정 Key 와 해당 Value 를 삭제하기 위해서는 delete() 함수를 사용한다.
- Map Key Check
Map 을 사용할 때 종종 Map 안에 특정 Key 가 존재하는지 체크할 필요가 있다. 이를 위해 Go 에서 map[Key]
읽기를 수행할 때 2개의 값을 Return 한다. 첫번째는 해당 Key 에 상응하는 Value 이고, 두 번째는 그 Key 가 존재하는지 아닌지를 나타내는 Bool 값이다.
package main
func main() {
tickers := map[string]string {
"GOOG": "Google Inc",
"MSFT": "MicroSoft",
"FB": "FaceBook",
"AMZN": "Amazon",
}
// map Key 체크
val, exists := tickers["MSFT"]
if !exists {
println("No MSFT ticker")
} else {
println(val)
}
}
- For Loop 를 사용한 Map 열거
Map 에 저장된 모든 요소를 출력하기 위해, for range Loop 를 사용할 수 있다. Map 컬렉션에 for range 를 사용하면, Key 와 Value 2개의 데이터를 Return 한다.
package main
import "fmt"
func main() {
myMap := map[string]string {
"A": "Apple",
"B": "Banana",
"C": "Charlie",
}
// for range 문을 사용한 모든 Map 요소 출력
// Map 은 Unordered 이기 때문에 순서는 무작위
for key, val := range myMap {
fmt.Println(key, val)
}
}
Go package
Go 는 패키지를 통해 Code 의 모듈화, 재사용 기능을 제공한다. Go 는 패키지를 사용해 작은 단위의 컴포넌트를 작성하고, 이러한 작은 패키지를 활용해 프로그램을 작성할 것을 권장한다.
Go 에서 사용하는 표준 패키지는 Go Packages 에 자세히 설명되어 있다.
- Main Package
일반적으로 패키지는 라이브러리로 사용되지만, “main” 이라 명명된 패키지는 Go Compiler 에 의해 특별하게 인식된다. 패키지명이 main 인 경우, 컴파일러는 해당 패키지를 공유 라이브러리가 아닌 Executable Program 으로 만든다. 그리고 이 패키지 안의 main() 함수가 프로그램의 시작점이 된다. 패키지를 공유 라이브러리로 만들고자 할 때, main 패키지나 main 함수를 사용해서는 안 된다.
- Package import
다른 패키지를 프로그램에서 사용하기 위해 Import 를 사용한다. 예를 들어, Go 의 표준 라이브러리인 fmt 패키지를 사용하기 위해서, 다음과 같이 import 문을 사용할 필요가 있다. Import 후에는 예제와 같이 해당 패키지의 함수를 호출해 사용할 수 있다.
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
- Package Scope
패키지 내에는 함수, 구조체, 인터페이스, 메소드 등이 존재하는데, Identifier 의 첫문자가 대문자로 시작하면 이는 public 으로 사용할 수 있다. 반면, 이름이 소문자로 시작하면 non-public 으로 패키지 내부에서만 사용될 수 있다.
- Package Init 함수와 Alias
패키지를 작성할 때, 패키지 실행시 처음으로 호출되는 init() 함수를 작성할 수 있다.
package testlib
var pop map[string]string
func init() {
pop = make(map[string]string)
}
경우에 따라 패키지를 Import 하면서 해당 패키지 안의 Init() 함수만을 호출하고자 하는 케이스가 있다. 이런 경우는 패키지 Import 시 _ 라는 alias 를 지정한다.
package main
import _ "other/xlib"
만약 패키지 이름이 동일하지만, 서로 다른 버전 혹은 서로 다른 위치에서 로딩하고자 할 때는, 패키지 alias 를 사용해 구분할 수 있다.
import (
mongo "other/mongo/db"
mysql "other/mysql/db"
)
func main() {
mondb := mongo.Get()
mydb := mysql.Get()
}
- 사용자 정의 패키지 생성
개발자는 사용자 정의 패키지를 만들어 재사용 가능한 Component 를 만들 수 있다. 사용자 정의 라이브러리 패키지는 일반적으로 폴더를 하나 만들고, 그 폴더 안에 .go 파일들을 만들어 구성한다. 하나의 Sub 폴더 안에 있는 .go 파일들은 동일한 패키지명을 가지며, 패키지명은 해당 폴더의 이름과 같게 한다. 즉, 해당 폴더에 있는 여러 *.go
파일들은 하나의 패키지로 묶인다.
package testlib
import "fmt"
var pop map[string]string
func init() {
println("TestLib initialization")
pop = make(map[string]string)
pop["Adele"] = "Hello"
pop["Alicia Keys"] = "Falling'"
pop["John Legend"] = "All of Me"
}
func GetMusic(singer string) string {
return pop[singer]
}
func getKeys() {
for _, kv := range pop {
fmt.Println(kv)
}
}
Optional : Size 가 큰 라이브버리의 경우, go install
명령을 사용해 라이브러리를 컴파일하여 Cache 할 수 있는데, 이렇게 하면 다음 빌드시 빌드 타임을 크게 줄일 수 있다. Go 패키지를 빌드하고 /pkg 폴더에 인스톨하기 위해 go install
명령어를 라이브러리 폴더 내에서 실행할 수 있다.
package main
import "../testlib"
func main() {
println("Hi")
song := testlib.GetMusic("Alicia Keys")
println(song)
}
Struct (구조체)
Go 에서 struct 는 Custom Data Type 을 표현하는데 사용되는데, Go 의 struct 는 필드 데이터만을 가지며 메소드를 갖지 않는다.
Go 언어는 OOP 를 고유의 방식으로 지원한다. 즉, Go 에는 전통적인 OOP 언어가 갖는 클래스, 객체, 상속의 개념이 없다. 전통적인 OOP 의 클래스는 Go 에서 Custom 타입을 정의하는 Struct 로 표현되는데, 전통적인 OOP 와 달리 Go 언어의 Struct 는 필드만을 가지며, 메소드는 별도로 분리해 정의한다. (Go Method) 에서 설명
- Struct 선언
Struct 를 정의하기 위해서는 Custom type 정의에 사용하는 Type 문을 사용한다. 예를 들어 Name 과 age 필드를 갖는 Person 이라는 struct 를 정의하기 위해서는 아래와 같이 Type 문을 사용한다. 만약 이 Person 구조체를 패키지 외부에서 사용할 수 있게 하려면, Struct 명을 Person
으로 선언하면 된다.
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
p := person{}
p.name = "Lee"
p.age = 10
fmt.Println(p)
}
- Struct 객체 생성
선언된 Struct 타입으로부터 객체를 생성하는 방법은 여러 가지가 있다. 위와 같이 person{} 을 사용해 빈 Person 객체를 먼저 할당하고, 나중에 그 필드값을 채워넣는 방법이 있다.
Struct 객체를 생성할 때, 초기값을 함께 할당하는 방법도 있다. 아래의 예제처럼, Struct 필드값을 순서대로 {}
괄호 안에 넣을 수 있으며, 순서에 상관없이 필드명을 지정해 그 값을 넣을 수도 있다. 특히, 필드명을 지정할 때 일부 필드가 생략된 경우, 생략된 필드들은 Zero Value 를 갖는다.
var p1 person
p1 = person{"Bob", 20}
p2 := person{name: "Sean", age: 50}
또 다른 객체 생성 방버으로 Go 내장함수 new() 를 사용할 수 있다. new() 를 사용하면 모든 필드를 Zero Value 로 초기화하고, Person 객체의 포인터를 리턴한다. 객체 포인터인 경우에도 필드를 접근할 때, .
(dot) 를 사용하는데, 이 때 포인터는 자동으로 Dereference 된다.
p := new(person)
p.name = "Lee"
Go 에서 struct 는 기본적으로 Mutable 개체로 필드값이 변경될 경우, 해당 개체의 메모리에서 직접 변경된다. 하지만, Struct 개체를 다른 함수의 Parameter 로 전달하면, Pass by Value 에 따라 객체를 복사해 전달하게 된다. 그래서 만약 Pass by Reference 로 Struct 를 전달하고자 한다면, 포인터를 전달해야 한다.
- 생성자 (Constructor) 함수
때로 Struct 의 필드가 사용 전에 초기화되어야 하는 경우가 있다. 예를 들어, struct 의 필드가 Map 타입인 경우 Map 을 사전에 미리 초기화해 놓으면, 외부 Struct 사용자가 매번 Map 을 초기화해야 한다는 것을 기억할 필요가 없다. 이러한 목적을 위해 생성자 함수를 사용할 수 있다.
아래 예제에서 생성자 newDict() 는 dict 라는 struct 의 Map 필드를 초기화한 후 그 Struct 의 포인터를 Return 한다. 이어 main() 함수에서 struct 개체를 만들 때 dict 를 직접 생성하지 않고, 대신 newDict() 함수를 호출해 이미 초기화된 Data 맵 필드를 사용한다.
package main
type dict struct {
data map[int]string
}
//생성자 함수 정의
func newDict() *dict {
d := dict{}
d.data = map[int]string{}
return &d //포인터 전달
}
func main() {
dic := newDict() // 생성자 호출
dic.data[1] = "A"
}
Reference : 예제로 배우는 Go 프로그래밍