go에서는 자료형에 새로 이름을 붙일 수 있습니다. 사실 rune형은 int32의 별칭입니다. 이런 자료형을 명명된 자료형(Named Type)이라고 합니다. 

type rune int32


엄밀히 말해서 int32 역시 명명된 자료형에 속합니다. 그러면 명명되지 않은 자료형 (Unnamed Type)은 어떤 것들이 있을까요

type runes []rune
type MyFunc func() int

위와 같이 runes와 MyFunc는 이름 자체만으로 자료형을 지칭하는 것이기에 명명된 자료형이 되고, []rune과 func() int는 이름만으로 자료형을 지칭하는 것이 아니기 때문에 명명되지 않은 자료형으로 구분됩니다. 명명된 자료형과 명명되지 않은 자료형 모두에 type 예약어를 사용하여 새 이름을 붙여줄 수 있습니다. 


type Test1 int
type Test2 int

위와 같이 int 타입의 서로 다른 명명된 자료형을 선언한 다음 비교를 하면 서로 다른 타입으로 나옵니다. 한마디로 명명된 자료형끼리는 호환이 되지 않습니다. 비교, 또는 반환을 할 때는 해당 타입으로 변환을 해줘야 가능합니다.


자료형에 이름을 붙이면 자료형을 검사함으로써 프로그램을 직접 수행하기 전에 컴파일 시점에서 버그를 어느정도 예방할 수 있습니다.

아래는 클로저를 사용하여 생성기(generator)를 만든 예제입니다.

package main
import (
	"fmt"
)
func NewIntGenerator() func() int {
	var next int
	return func() int {
		next++
		return next
	}
}
func main() {
	gen := NewIntGenerator()
	fmt.Println(gen(), gen(), gen(), gen(), gen())
	fmt.Println(gen(), gen(), gen(), gen(), gen())
}
// 결과
// 1 2 3 4 5
// 6 7 8 9 10

NewIntGenerator()는 함수를 반환하는 고계함수입니다. 또한 이 함수가 반환하는 함수는 클로저입니다. 반환하는 함수 리터럴(람다함수 형태처럼 보이는 함수)이 속해 있는 스코프 안에 있는 next 변수에 접근하고 있습니다. 그러면 이 함수는 next 변수와 함께 세트로 묶입니다. 만약 NewIntGenerator()를 여러 번 호출하여 함수를 여러 개를 갖고 있으면 각각의 함수가 갖고 있는 next는 따로 분리되어 있습니다. 한마디로 gen이라는 변수안에 NewIntGenerator() 함수를 담고 있기 때문에 gen변수를 계속 호출하면 반환되는 함수 리터럴과 함께 세트로 묶여있는 next 변수의 값이 증가하게 됩니다. 만약 NewIntGenerator()()와 같이 따로 호출을 반복하면 next의 값은 1만 나오게 됩니다.


package main
import (
	"fmt"
)
func NewIntGenerator() func() int {
	var next int
	return func() int {
		next++
		return next
	}
}
func main() {
	gen := NewIntGenerator()
	gen2 := NewIntGenerator()
	fmt.Println(gen(), gen(), gen(), gen(), gen())
	fmt.Println(gen2(), gen2(), gen2(), gen2(), gen2())
	fmt.Println(gen(), gen(), gen(), gen(), gen())
}


// 결과
// 1 2 3 4 5
// 1 2 3 4 5
// 6 7 8 9 10

위 코드에서 gen과 gen2에 세트로 묶여 있는 next는 서로 다릅니다. 그렇기 때문에 gen을 호출하여 증가시킨 숫자와 gen2를 호출하여 증가시킨 숫자는 서로 다릅니다. gen, gen2 두 함수의 상태가 분리되어 있는 것입니다.


같은 방식을 이용하여 느긋한 계산법(lazy evaluation)을 구현하거나 무한한 크기의 자료구조를 만들 수도 있습니다.

말은 어렵지만 의미는 간단합니다. 함수를 파라미터 또는 인자값에 넣는 것입니다. 아래는 고계함수의 예제입니다.

func ReadFrom(r io.reader, f func(line string)) error {
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		f(scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return err
	}
	return nil
}
 
func main() {
	r:= strings.NewReader("bill\ntom\njani\n")
	err := ReadFrom(r, func(line string) { // 람다함수와 같은 기능
		fmt.Println("(", line, ")")
	})
	if err != nil {
		fmt.Println(err)
	}
}
// 결과
// (bill)
// (tom)
// (jani)


Go언어에는 상수 시간에 키의 존재를 확인할 수 있는 집합은 따로 제공하고 있지 않습니다. 여기서 가장 단순한 방법은 맵을 이용하면서 값을 bool형으로 주는 것입니다. 문자열에 중복되어 들어가 있는 글자가 있는지 검사하는 함수를 만드는 예제입니다.

package main


import "fmt"

func hasDupeRune(s string) bool {
	runeSet := map[rune]bool{}
	for _, r := range s {
		if runeSet[r] {
			return true
		}
		runeSet[r] = true
	}
	return false
}


func main() {
	test := "가나다라"
	fmt.Println(hasDupeRune(test))


	test1 := "가나다라가"
	fmt.Println(hasDupeRune(test1))
}


# 결과
false
true


코드는 깔끔하지만 불필요한 bool값 때문에 메모리를 차지한다는 단점이 있습니다. 이것을 해결하려면 빈 구조체를 값으로 사용하면 됩니다. 값 부분의 메모리를 따로 차지하지 않게 되어 있기 때문에 오버헤드가 존재하지 않습니다. 

여기서 문제는 특정 값이 있는지 없는지를 어떻게 아는지 입니다. 키가 있거나 없거나 맵에서 읽으면 빈 구조체가 나오기 때문입니다. 맵을 읽을 때 값을 2개로 받을 수 있습니다. 그때 두 번째 반환값을 조사하면 키가 있는지 없는지 알 수 있습니다. 따라서 위의 코드를 다음과 같이 변경할 수 있습니다.

package main


import "fmt"

func hasDupeRune(s string) bool {
	runeSet := map[rune]struct{}{}
	for _, r := range s {
		if _, exists := runeSet[r]; exists {
			return true
		}
		runeSet[r] = struct{}{}
	}
	return false
}

func main() {
	test := "가나다라"
	fmt.Println(hasDupeRune(test))
	test1 := "가나다라가"
	fmt.Println(hasDupeRune(test1))
}


struct부분에 대괄호를 열고 닫는 것이 두번 반복하니 이상해 보입니다. 일단 struct{}는 아무 필드가 없는 구조체이므로 값이 아니라 자료형입니다. 맵을 만들 때 맵의 자료형은 map[rune]struct가 되는 것이 아니라 map[rune]struct{}가 되므로 이 맵에 아무것도 안 들어 있는 상태로 초기화를 하려면 위와 같이 map[rune]struct{}{}를 해주어야 하는 것입니다. 그리고 다시 말해 struct{}는 빈 구조체 자료형이므로 해당 구조체에 아무 내용이 없는 상태의 값을 표현하려면 struct{}{}와 같이 표현을 해서 runeSet[r]=struct{}{}와 같이 써야 합니다.


맵과 집합에서 다루지 않은 하나가 더 있는데 삭제하는 방법입니다.

delete(m, key)

이렇게 하면 m[key]=기본값과 달리 아예 키가 사라지게 됩니다. 원래 맵에 키가 들어 있었으면 len(m)의 값이 1 감소하게 됩니다.

아래의 두 예제는 같은 의미입니다.

1.

if v, err := Read(); err == nil {
	// do something with v, because Read() did not
	// had an error (err)
}

2.

v, err := Read()
if err == nil {
	// do something with v
}


두 문법의 결과는 같지만 차이점은 첫 번째 예제에서는 v와 err의 범위는 if {}블록으로 제한됩니다. 두 번째 예제에서는 v와err은 선언된 블록에 속해 있습니다. 즉, if {}에 속해있지 않다는 뜻입니다. {}블록에서 ;을 사용하는 것과 거의 같습니다.

if문에 ;을 사용할 땐, 선언되는 변수가 조건문 안에서만 사용된다고 확실시 될 때 사용하면 좋습니다.

Go 패키지를 볼 때, 아래와 같은 문법을 사용한 함수를 심심찮게 볼 수 있습니다.

func GetCommandLineMultiArgs(args ... string) {
   arguments := make(map[string]*string)

   for _, arg := range args {
      tempVar := flag.String(arg, "", arg)
      arguments[arg] = tempVar
   }

   flag.Parse()
}


...의 의미는 파이썬의 *args와 유사합니다. 파라미터로 입력된 타입의 데이터를 몇 개 받을지 확실하지 않을 때 사용합니다.

아래는 String 타입의 데이터를 받는 예제입니다.

package main
import "fmt"
 
func main() {
	test("111")
	test("111","222","333")
}
 
func test(args ... String) {
	fmt.Println(args)
}
 
# 결과
# [111]
# [111 222 333]


위와같이 슬라이스로 생성된 스트링 리스트를 반환하게 됩니다.


위에서 정의한 test()함수에 이미 존재하는 슬라이스 타입을 전달하려면 아래와 같이 선언합니다.

package main
import "fmt"
 
func main() {
	nums := []string{"111", "222", "333"}
	test(nums...)
}
 
func test(args ... String) {
	fmt.Println(args)
}
 
# 결과
#[111 222 333]


'언어 > GO' 카테고리의 다른 글

[GO] map에서 key값 존재유무 확인  (0) 2017.02.02
[GO] if문에 세미콜론(;) 의미  (0) 2017.02.02
[GO] 파라미터 ... 의미  (0) 2017.01.18
[GO] init() 함수 호출 시점  (0) 2017.01.18
[GO]슬라이스 용량 주의사항  (0) 2017.01.18
[GO] 현재 폴더 경로 확인  (0) 2017.01.17

+ Random Posts