Skip to content

타입

이전의 데이터 타입 섹션에서 Go 의 모든 내장 데이터 타입을 간단히 소개했습니다. 이러한 내장 기본 타입은 이후 사용자 정의 타입의 기초가 됩니다. Go 는 전형적인 정적 타입 언어로, 모든 변수의 타입은 컴파일 시기에 결정되며 프로그램의 전체 수명 주기 동안 변경되지 않습니다. 이 섹션에서는 Go 의 타입 시스템과 기본 사용법을 간단히 소개하겠습니다.

정적 강타입

Go 는 정적 강타입 언어입니다. 정적이라는 것은 Go 의 모든 변수 타입이 컴파일 시기에 이미 결정되며 프로그램의 수명 주기 동안 변경되지 않는다는 것을 의미합니다. Go 의 단축 변수 선언이 동적 언어의 문법과 유사해 보이지만, 변수 타입은 컴파일러가 자동으로 추론하며, 근본적인 차이점은 타입이 한 번 추론된 후 변경되지 않는다는 점입니다. 동적 언어는 완전히 반대입니다. 따라서 아래 코드는 컴파일을 통과할 수 없습니다. a 는 int 타입 변수이므로 문자열을 할당할 수 없습니다.

go
func main() {
  var a int = 64
  a = "64"
  fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}

강타입은 프로그램에서 엄격한 타입 검사를 수행한다는 것을 의미하며, 타입이 일치하지 않는 상황이 발생하면 동적 언어처럼 가능한 결과를 추측하는 대신 즉시 프로그래머에게 오류를 알립니다. 따라서 아래 코드는 컴파일을 통과할 수 없습니다. 두 타입이 다르므로 연산을 수행할 수 없습니다.

go
func main() {
  fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}

타입 후치

Go 가 타입 선언을 앞쪽이 아닌 뒤쪽에 두는 이유는 대부분 C 언어에서 교훈을 얻었기 때문입니다. 공식의 예시를 들어 효과를 살펴보겠습니다. 이는 함수 포인터입니다.

c
int (*(*fp)(int (*)(int, int), int))(int, int)

솔직히 말해 주의 깊게 보지 않으면 이것이 어떤 타입인지 알기 어렵습니다. Go 에서 유사한写法는 다음과 같습니다.

go
f func(func(int,int) int, int) func(int, int) int

Go 의 선언 방식은 항상 이름이 앞에 있고 타입이 뒤에 있는 원칙을 따릅니다. 왼쪽에서 오른쪽으로 읽으면, 첫눈에 이것이 함수이며 반환 값이 func(int,int) int 임을 알 수 있습니다. 타입이 더 복잡해질수록 타입 후치는 가독성 면에서 훨씬 낫습니다. Go 의 많은 설계는 가독성을 위해 서비스됩니다.

타입 선언

Go 에서 타입 선언을 통해 사용자 정의 이름의 새 타입을 선언할 수 있습니다. 새 타입을 선언하려면 일반적으로 타입 이름과 기본 타입이 필요합니다. 간단한 예시는 다음과 같습니다.

go
type MyInt int64

위의 타입 선언에서 type 키워드를 사용하여 기본 타입이 int64이고 이름이 MyInt인 타입을 선언했습니다. Go 에서 모든 새로 선언된 타입은 대응하는 기본 타입이 있어야 하며, 타입 이름은 기존 내장 식별자와 중복되지 않는 것이 좋습니다.

go
type MyInt int64

type MyFloat64 float64

type MyMap map[string]int

// 컴파일은 통과하지만 권장하지 않습니다. 기존 타입을 덮어쓰게 됩니다.
type int int64

타입 선언을 통해 선언된 타입은 모두 새 타입이며, 기본 타입이 동일하더라도 서로 다른 타입은 연산을 수행할 수 없습니다.

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)
go
invalid operation: f1 + f (mismatched types MyFloat64 and float64)

타입 별명

타입 별명은 타입 선언과 다릅니다. 타입 별명은 새 타입을 생성하는 것이 아니라 단순히 별명일 뿐입니다. 간단한 예시는 다음과 같습니다.

go
type Int = int

둘은 동일한 타입이며 이름만 다르므로 연산을 수행할 수 있습니다. 따라서 아래 예시도 컴파일을 통과할 수 있습니다.

go
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)
3

타입 별명은 특히 복잡한 타입에 유용합니다. 예를 들어 map[string]map[string]int라는 타입이 있다고 가정합니다. 이는 2 차원 map 입니다. 이제 함수 매개변수가 map[string]map[string]int 타입이라고 가정합니다.

go
func PrintMyMap(mymap map[string]map[string]int) {
   fmt.Println(mymap)
}

이러한 경우 타입 선언을 사용할 필요가 없습니다. 전자는 새 타입을 선언하므로 해당 함수의 매개변수로 사용할 수 없기 때문입니다. 타입 별명을 사용한 후의 예시는 다음과 같습니다.

go
type TwoDMap = map[string]map[string]int

func PrintMyMap(mymap TwoDMap) {
   fmt.Println(mymap)
}

타입 별명을 사용하면 더 간결해 보입니다.

TIP

내장 타입 anyinterface{}의 타입 별명이며, 둘은 완전히 동일하고 이름만 다릅니다.

타입 변환

Go 에서는 명시적 타입 변환만 존재하며, 암시적 타입 변환은 존재하지 않습니다. 따라서 서로 다른 타입의 변수는 연산을 수행할 수 없으며 매개변수로 전달할 수 없습니다. 타입 변환은 변환될 변수의 타입과 변환할 대상 타입을 알고 있는 경우에만 적용 가능합니다. 예시는 다음과 같습니다.

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
0.30000000000000004

MyFloat64float64 타입으로 명시적으로 변환해야만 덧셈 연산을 수행할 수 있습니다. 타입 변환의 또 다른 전제는 변환될 타입이 대상 타입으로 표현 가능해야 한다는 것입니다 (Representability). 예를 들어 intint64 타입으로 표현될 수 있으며 float64 타입으로도 표현될 수 있으므로 이들 간에 명시적 타입 변환을 수행할 수 있지만, int 타입은 stringbool 타입으로 표현될 수 없으므로 타입 변환을 수행할 수 없습니다.

TIP

표현 가능성 (Representability) 에 대한 정의는 참조 매뉴얼 - Representability 에서 자세한 내용을 확인할 수 있습니다.

두 타입이 서로 표현 가능하더라도 타입 변환 결과가 항상 올바른 것은 아닙니다. 아래 예시를 보십시오.

go
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
1 0

num1은 올바르게 int32 타입으로 변환되었지만, num2는 그렇지 않았습니다. 이는 전형적인 수치 오버플로우 문제입니다. int32는 31 비트 정수를 표현할 수 있고, int8은 7 비트 정수만 표현할 수 있습니다. 고정밀 정수가 저정밀 정수로 변환될 때는 상위 비트를 버리고 하위 비트를 유지하므로, num1의 변환 결과는 0 이 됩니다. 숫자 타입 변환에서는 일반적으로 작은 것을 큰 것으로 변환하는 것을 권장하며, 큰 것을 작은 것으로 변환하는 것은 권장하지 않습니다.

타입 변환을 사용할 때 일부 타입에 대해 모호함을 피해야 합니다. 예시는 다음과 같습니다.

go
*Point(p) // *(Point(p)) 와 동일
(*Point)(p)  // p 를 *Point 타입으로 변환
<-chan int(c)    // <-(chan int(c)) 와 동일
(<-chan int)(c)  // c 를 <-chan int 타입으로 변환
(func())(x)      // x 를 func() 타입으로 변환
(func() int)(x)  // x 를 func() int 타입으로 변환

타입 단언

타입 단언은 일반적으로某一인터페이스 타입 변수가某一타입에 속하는지 판단하는 데 사용됩니다. 예시는 다음과 같습니다.

go
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
   fmt.Println(intVal)
} else {
   fmt.Println("error type")
}
1

interface{}는 공백 인터페이스 타입이며, 공백 인터페이스 타입은 모든 타입을 표현할 수 있지만, int 타입은 interface{} 타입을 표현할 수 없으므로 타입 변환을 사용할 수 없습니다. 타입 단언을 사용하면 하위 타입이 원하는 타입인지 판단할 수 있습니다. 타입 단언 문은 두 개의 반환 값을 가지며, 하나는 타입 변환된 값이고 다른 하나는 변환 결과의 불리언 값입니다.

타입 판단

Go 에서 switch 문은 특별한写法를 지원합니다. 이러한写法를 통해 서로 다른 case에 따라 서로 다른 로직 처리를 할 수 있습니다. 사용 전제는 입력 매개변수가 반드시 인터페이스 타입이어야 한다는 것입니다. 예시는 다음과 같습니다.

go
var a interface{} = 2
switch a.(type) {
    case int: fmt.Println("int")
    case float64: fmt.Println("float")
    case string: fmt.Println("string")
}
int

TIP

unsafe 패키지에서 제공하는 연산을 사용하면 Go 의 타입 시스템을 우회하여 원래 컴파일을 통과할 수 없었던 타입 변환 연산을 수행할 수 있습니다.

Golang by www.golangdev.cn edit