Day 4 with Go-Lang

4 minute read

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 프로그래밍