Golang/Basic

Zero 부터 시작하는 Golang - 기본문법1

시바도지 2022. 2. 13. 17:13
반응형

 

클로저(Closure)


  • 클로저는 함수 안에서 함수를 선언 및 정의할 수 있고, 바깥쪽 함수에 선언된 변수에도 접근할 수 있는 함수를 말한다.
  • 바깥 함수가 변수와 자기 자신(함수)을 둘러싸고(close over)있다고 해서 클로저(closure)라고 한다.

아래의 예제는 nextValue() 함수는 int를 반환하는 익명 함수(func() int)를 반환하는 함수이다.

Go언어에서 함수는 일급 함수로서 다른 함수로 부터 반환되는 반환 값으로 사용될 수 있다.

그런데 여기서 이 익명 함수가 그 함수 바깥에 있는 변수 i를 참조하고 있다.

익명 함수 자체가 지역 변수로 i를 갖는 것이 아니기 때문에(만약 그렇게 되면 함수 호출 시 i는 항상 0으로 설정된다.)

외부 변수 i가 상태를 계속 유지하는 값을 계속 하나씩 증가시키는 기능을 하게 된다.

package main

func nextValue() func() int {
	i := 0

	return func() int {
		i++
		return i
	}
}

func main() {
	next := nextValue()
	println(next()) // 1
	println(next()) // 2
	println(next()) // 3
	anotherNext := nextValue()
	println(anotherNext) // 1 (i값 초기화)
	println(anotherNext) // 2
}

 

배열


  • 배열의 선언은 'var 변수명 [배열 크기] 자료형' 과 같이 한다.
package main

func main() {
	var a [3] int // 정수형 3개 요소를 갖는 배열 a 선언
	a[0] = 1
	a[1] = 2
	a[2] = 3

	println(a[1]) // 2 출력
}

배열 초기화

  • 초기 값은 '[배열 크기] 자료형' 뒤에 {}(Curly Brackets)을 두고 초기 값을 순서대로 적는다.
  • 만약 초기화 과정에서 [...]를 사용하여 배열 크기를 생략하면 자동으로 초기화 요소 숫자만큼 배열 크기가 정해진다.
var a1 = [3]int{1, 2, 3}
var a2 = [...] int{1, 2, 3} // 배열 크기가 자동으로 지정됨

다 차원 배열

  • 다 차원 배열은 배열 크기 부분을 여러 개로 설정하여 선언한다.

예를 들어 3 X 4 X 5인 3차원 정수 배열을 만들려면, 다음과 같이 배열을 정의한다.

다 차원 배열의 사용은 일 차원 배열과 유사하게 각 차원 별 배열 인덱스를 지정하여 사용한다.

var multiArray [3][4][5]int // 정의
multiArray[0][1][2] = 10    // 사용

다 차원 배열의 초기화

다 차원 배열의 초기화는 일 차원 배열의 초기화와 비슷하다.

하지만, 다 차원이기 때문에 배열 초기 값 내에 다시 배열 값을 넣는 형태이다.

아래의 예제는 2개의 행과 3개의 열을 이루는 배열이 초기화 되었다.

1행이 {1, 2, 3} 이고 2행이 {4, 5, 6} 로 이루어져 있다.

func main() {
	var a= [2][3] int {
		{1, 2, 3},
		{4, 5, 6}, // 끝에는 Comma를 추가해 준다.
	}

	println(a[1][2]) // 6 출력
}

 

슬라이스(Slice)


  • Go 언어의 배열은 고정된 배열 크기 내에 동일한 타입의 데이터를 연속적으로 저장하지만, 배열의 크기를 동적으로 증가 시키거나부분 배열을 추출하는 등의 기능을 가지고 있지는 않다.
  • Go 슬라이스는 내부적으로 배열에 기초하여 만들어 졌지만 배열의 이런 제약점들을 넘어 개발자에게 편리하고 유용한 기능들을 제공한다.
  • 슬라이스는 배열과 달리 고정된 크기를 미리 지정하지 않을 수 있고, 크기를 동적으로 변경할 수 있고, 또한 부분 배열을 추출할 수도 있다.

Go 슬라이스 선언은 배열을 선언하는 것과 같이 'var 변수명 [] 자료형' 처럼 하는데 배열과 다르게 크기는 지정하지 않는다.

아래의 예제는 정수형 슬라이스 변수 a를 선언하였다.

package main

import "fmt"

func main() {
	var a []int           // 슬라이스 변수 선언
    
	a    = []int{1, 2, 3} // 슬라이스에 리터럴값 지정
	a[1] = 10
    
	fmt.Println(a)        // [1, 10, 3]출력
}

Go에서 슬라이스를 생성하는 또 다른 방법으로 Go의 내장 함수 make() 함수를 이용할 수 있다.

make() 함수로 슬라이스를 생성하면, 개발자가 슬라이스의 길이(Length)와 용량(Capacity)을 임의로 지정할 수 있는 장점이 있다.

make() 함수의 첫 번째 매개변수에 생성할 슬라이스 타입을 지정하고, 두번 째는 Length(슬라이스의 길이), 그리고 세번 째는 Capacity(내부 배열의 최대 길이)를 지정하면,

모든 요소가 Zero Value인 슬라이스를 만들게 된다.

여기서 만약 세번 째 Capacity 매개변수를 생략하면 Capacity는 Length와 같은 값을 갖는다.

그리고 슬라이스의 길이 와 용량은 내장함수 len(), cap()을 써서 확인할 수 있다.

func main() {
	s := make([]int, 5, 10)
    
	println(len(s), cap(s)) // len 5, cap 10
}

슬라이스에 별도의 길이와 용량을 지정하지 않으면, 기본적으로 길이와 용량이 0인 슬라이스를 생성하는데, 이를 Nil Slice라 하고,

Nil과 비교하면 true를 반환한다.

func main() {
    var s []int

    if s == nil {
        println("Nil Slice")
    }

    println(len(s), cap(s)) // 0 0
}

부분 슬라이스(Sub-Slice)

슬라이스에서 요소 일부를 추출할 수 있다.

부분 슬라이스는 '슬라이스[처음 인덱스 : 마지막 인덱스]' 형식으로 만든다.

아래의 예제는 슬라이스 s에 대해 인덱스 2부터 4까지 데이터를 갖는 부분 슬라이스를 만들려면,

s[2:5]와 같이 표현한다. 여기서 [2:4]가 아니라 [2:5]으로 쓰는데, 마지막 인덱스는 원하는 인덱스 + 1을 사용한다.

즉, 처음 인덱스는 Inclusive 이며, 마지막 인덱스는 Exclusive 이다. (Python과 동일하다.)

package main

import "fmt"

func main() {
	s := []int{0, 1, 2, 3, 4, 5}
    
	s = s[2:5]
    
	fmt.Println(s) // 2,3,4 출력
}

슬라이스 인덱스는 처음, 마지막 둘 중 또는 둘 다 생략할 수 있다.

처음 인덱스가 생략되면 0 이,

마지막 인덱스가 생략되면 그 슬라이스의 마지막 인덱스가 자동으로 대입 된다.

 

아래의 예제는 처음 0 부터 인덱스 4까지를 포함하기 위해서는 [:5]를,

인덱스 2부터 마지막까지 포함하기 위해서는 [2:]와 같이 쓸 수 있다.

만약 [:]와 같이 모두 생략한다면, 전체를 표현한다.

s := []int{0, 1, 2, 3, 4, 5}
s = s[2:5]     // 2, 3, 4
s = s[1:]      // 3, 4
fmt.Println(s) // 3, 4 출력

슬라이스 추가, 병합(append)과 복사

배열은 고정된 크기로 그 크기 이상의 데이터를 임의로 추가할 수 없지만, 슬라이스는 자유롭게 새로운 요소를 추가할 수 있다.

슬라이스에 새로운 요소를 추가하기 위해서는 Go 내장함수인 append()를 사용하면된다.

append()의 첫 매개변수는 슬라이스 객체이고, 두 번째는 추가할 요소의 값이다.

또한 여러 개의 요소 값들을 한꺼번에 추가하기 위해서는 append() 두 번째 매개변수 뒤에 계속하여 값을 추가할 수 있다.

func main() {
	s := []int{0, 1}

	// 하나 확장
	s = append(s, 2)        // 0, 1, 2

	// 복수 요소들 확장
	s = append(s, 3, 4, 5)  // 0, 1, 2, 3, 4, 5
	
    fmt.Println(s)
}

내장함수 append()가 슬라이스에 데이터를 추가할 때, 내부적으로 다음과 같은 일이 일어난다.

슬라이스 용량(Capacity)이 아직 남아 있는 경우는 그 용량 내에서 슬라이스의 길이(Length)를 변경하여 데이터를 추가하고, 용량(Capacity)을 초과하는 경우

현재 용량의 2배에 해당하는 새로운 Underlying array를 생성하고 기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당한다.

 

아래의 예제는 길이 0, 용량 3의 슬라이스에 1부터 15까지의 숫자를 계속 추가하면서 슬라이스의 길이와 용량이 어떻게 변하는 지를 확인하는 코드이다.

코드를 실행하면 1~3까지는 기존의 용량 3을 사용하고, 4~6까지는 용량 6을, 7~12는 용량 12, 그리고 13~15는 용량 24의 슬라이스가 사용되고 있음을 알 수 있다.

package main

import "fmt"

func main() {
	// len=0, cap=3 인 슬라이스
	sliceA := make([]int, 0, 3)

	// 계속 한 요소씩 추가
	for i := 1; i <= 15; i++ {
		sliceA = append(sliceA, i)
	
		// 슬라이스 길이와 용량 확인
		fmt.Println(len(sliceA), cap(sliceA))
	}

	fmt.Println(sliceA) // 1 부터 15 까지 숫자 출력
}

한 슬라이스를 다른 슬라이스 뒤에 병합하기 위해서는 아래 예제와 같이 append()를 사용한다.

이 append() 함수에서는 2개의 슬라이스를 매개변수로 갖는데, 처음 슬라이스 뒤에 두 번째 매개변수의 슬라이스를 추가하게 된다.

여기서 한가지 주의할 것은 두 번째 슬라이스 뒤에 ... 을  붙인다는 것인데, 이 ...(Ellipsis)는 해당 슬라이스의 컬렉션을 표현하는 것으로 두 번째 슬라이스의 모든 요소들의

집합을 나타낸다.

아래 에제에서 sliceB...는 4, 5, 6으로 치환된다고 볼 수 있다.

package main

import "fmt"

func main() {
	sliceA := []int{1, 2, 3}
	sliceB := []int{4, 5, 6}
	sliceA = append(sliceA, sliceB...)
	//sliceA = append(sliceA, 4, 5, 6)
    
	fmt.Println(sliceA) // [1 2 3 4 5 6] 출력
}

이러한 추가, 확장 기능과 더불어 Go 슬라이스는 내장함수 copy()를 사용하여 한 슬라이스를 다른 슬라이스로 복사할 수도 있다.

아래 예제는 3개의 요소를 갖는 소스 슬라이스를 그 2배의 크기, 즉 6개를 갖는 타겟 슬라이스로 복사하는 예를 보여준다.

(아래에서 설명하듯이 슬라이스는 실제 배열을 가리키는 포인터 정보만을 가지므로, 복사를 좀 더 정확히 표현하면, 소스 슬라이스가 갖는

배열의 데이터를 타겟 슬라이스가 갖는 배열로 복제하는 것이다.)

func main() {
	source := []int{0, 1, 2}
	target := make([]int, len(source), cap(source) * 2)
    
	copy(target, source)
    
	fmt.Println(target) // [0 1 2] 출력
	fmt.Println(len(target), cap(target)) // 3, 6 출력
}

슬라이스의 내부 구조

슬라이스는 내부적으로 사용하는 배열의 부분 영역인 세그먼트에 대한 메타 정보를 갖고 있다.

슬라이스는 크게 3개의 필드로 구성되어 있는데, 첫 번째 필드는 내부적으로 사용하는 배열에 대한 포인터 정보이고,

두번 째는 세그먼트의 길이를, 그리고 세 번째는 세그먼트의 최대 용량(Capacity)이다.

처음 슬라이스가 생성될 때, 만약 길이와 용량이 지정되었다면, 내부적으로 용량(Capacity)만큼의 배열을 생성하고,

슬라이스 첫 번째 필드에 그 배열의 처음 메모리 위치를 지정한다.

그리고 두 번째 길이 필드는 지정된(첫 배열 요소로 부터의) 길이를 갖게되고 세번 째 용량 필드는 전체 배열의 크기를 갖는다.

예를 들어, 아래 첫번 문장을 보면, 0부터 5까지 6개의 요소를 갖는 슬라이스를 생성하고 있음을 볼 수 있다.

이때, 슬라이스의 배열포인터는 내부 배열의 첫번째 요소인 0을 가리키고, 길이는 전체 6, 그리고 용량도 6으로 설정된다.

 

예를 들어, 아래 첫번 문장을 보면, 0부터 5까지 6개의 요소를 갖는 슬라이스를 생성하고 있음을 볼 수 있다. 이때, 슬라이스의 배열포인터는 내부 배열의 첫번째 요소인 0을 가리키고, 길이는 전체 6, 그리고 용량도 6으로 설정된다.

그런데, 만약 이 슬라이스 S로 부터 Sub-slice S[2:5]를 하게 되면 S[2:5]는 인덱스 2부터 인덱스 4까지의 배열 요소를 가리키므로,

슬라이스 S의 배열 포인터는 세 번째 배열 요소인 2를 가리키고,

길이는 3을, 그리고 용량은 세 번째 배열 요소부터 배열 마지막까지 즉 4를 갖게 된다.

 

Map


  • Map은 Key에 대응하는 Value를 신속히 찾는 Hash Table을 구현한 자료구조이다.
  • Go언어는 Map 타입을 내장하고 있는데, 'map[Key 타입]value타입'과 같이 선언할 수 있다.

예를 들어 정수를 Key로하고 문자열을 Value로 하는  Map변수 idMap을 선언하기 위해서는 다음과 같이 할 수 있다.

var idMap map[int]string

이때 선언된 변수 idMap은 (Map은 Reference 타입이므로) Nil 값을 갖으며, 이를 Nil Map이라 부른다.

Nil map에는 어떤 데이터를 쓸 수 없는데, map을 초기화하기 위해 make()함수를 사용할 수 있다.

idMap = make(map[int]string)

make() 함수의 첫 번째 매개변수로 map 키워드와 [Key타입]Value타입을 지정하는데, 이때의 make() 함수는 Hash Table 자료구조를 메모리에 생성하고,

그 메모리를 가리키는 Map Value를 반환한다. (Map Value는 내부적으로 Runtime.hmap 구조체를 가리키는 포인터이다.)

따라서 idMap 변수는 이 Hash Table을 가리키는 Map을 가리키게 된다.

Map은 make() 함수를 써서 초기화할 수도 있지만, 리터럴(Literal)을 사용해 초기화 할 수도 있다.

리터럴 초기화는 'map[Key 타입]Value타입 {key : value}'와 같이 Map 타입 뒤 { }(Curly Brakets)안에 '키 : 값'들을 넣으면 된다.

// 리터럴을 사용한 초기화
tickers := map[string]string{
	"GOOG" : "Google Inc",
	"MSFT" : "Microsoft",
	"APPL" : "Apple Inc"
}

Map 사용

처음 map이 make() 함수에 의해 초기화 되었을 때는 아무런 데이터가 없는 상태이다.

이때 새로운 데이터를 추가하기 위해서는 'map변수[Key] = Value' 와 같이 해당 Key에 그 Value를 할당하면 된다.

아래의 예제에서 Key 101에 Apple을 할당하면 새로운 Hash Key-Value가 추가된다.

만약 Key 901의 값이 이미 존재했다면, 추가하는 대신에 Value만 갱신한다.

package main

func main() {
	var m map[int]string
	m = make(map[int]string)
	
	// 추가 또는 갱신
	m[101] = "Alpha"
	m[102] = "Bravo"
	m[103] = "Charlie"
	
	// 키에 대한 값 읽기
	str := m[102]
	println(str)
	noData := m[104] // 값이 없으면 nil 또는 zero 반환
	println(noData)
	
	// 삭제
	delete(m, 103)
}

map에서 특정 Key에 대한 Value를 읽을 때는 'map 변수[Key]'를 읽으면 된다.

위의 예제에서 m[102]는 Bravo라는 값을 str변수에 할당하게 된다.

만약 map안에 찾는 키가 존재하지 않는다면 Reference 타입인 경우 nil을 Value 타입인 경우 Zero를 반환한다.

map에서 특정 Key와 그 Value를 삭제하기 위해서는 delete() 함수를 사용한다.

Map 키 체크

map을 사용하는 경우 종종 map안에 특정 키가 존재 하는지를 확인할 필요가 있다.

이를 위해 Go에서는 'map 변수[Key]' 읽기를 수행할 때 2개의 리턴값을 반환한다.

첫 번째는 Key에 동등하는 값이고, 두 번째는 그 Key의 존재여부를 나타내는 bool값이다.

아래의 예제에서 val, exists := tickers["MSFT"]의 val에는 Microsoft라는 값이 반환되고, 변수 exitsts에는 Key가 존재하므로 true가 반환된다.

package main

func main() {
	tickers := map[string]string {
		"GOOG" : "Google Inc",
		"MSFT" : "Microsoft",
		"APPL" : "Apple Inc",
		"AMZN" : "Amazon",
	}

	// map 키 체크
	val, exists := tickers["MSFT"]

	if !exitst {
		println("No MSFT ticker")
	}

}

for 루프를 사용한 Map 열거

Map이 갖고 있는 모든 요소들을 출력하기 위해 for range 루프를 사용할 수 있다.

Map 컬렉션에 for range를 사용하면, Map Key와 Map Value 2개의 데이터를 반환한다.

아래 예제는 for 루프를 사용하여 Map으로 부터 Key와 Value를 하나씩 가져와서 출력하는 코드다.

package main

**import** "fmt"

func main() {
	myMap := map[string]string {
		"A" : "Alpha",
		"B" : "Bravo",
		"C" ; "Charlie",
	}

	// for range 문을 사용하여 모든 map 요소 출력
	// Map은 unordered 이므로 순서는 무작위다.
	for key, val := range myMap {
		fmt.Println(key, val)
	}
}

 

 

 

반응형