3장 타입

2장에서는 문자열이라는 데이터 타입(data type)을 이용해 Hello World를 저장했다. 데이터 타입은 관련 값의 집합을 분류하고 해당 값에 대한 연산을 기술하며, 값이 저장되는 방식을 정의한다. 타입은 한번에 이해하기에는 어려운 개념일 수 있으므로 Go에서 타입이 구현되는 방법을 살펴보기에 앞서 조금 다른 몇 가지 관점에서 타입을 살펴보겠다.

때때로 철학자들은 타입과 토큰을 구분하기도 한다. 예를 들어 맥스(Max)라는 개가 있다고 해보자. 맥스는 토큰(특별한 사례 또는 구성원)이고 개(일반적인 개념)는 타입이다. "개" 또는 "개라는 특징"은 모든 개가 공통적으로 지니고 있는 특성들을 기술한다. 과도하게 단순화한 면이 없지 않아 있지만 다음과 추론할 수 있을지도 모른다. 즉, 모든 개는 다리가 4개이며, 맥스는 개이므로, 맥스는 다리가 4개다. 프로그래밍 언어에서 타입은 이와 비슷한 방식으로 동작한다. 모든 문자열은 길이가 있으며, x는 문자열이고, 따라서 x는 길이를 가지고 있다.

수학에서는 종종 집합에 관해 이야기하기도 한다. 이를 테면, ℝ(모든 실수의 집합)이나 ℕ(모든 자연수의 집합)이 있다. 이러한 집합의 각 원소는 해당 집합의 다른 모든 원소와 특성을 공유한다. 예를 들어, 모든 자연수에 대해서는 결합 법칙이 성립한다. 즉, "모든 자연수 a, b, c에 대해 a + (b + c) = (a + b) + c와 a × (b × c) = (a × b) × c.가 성립한다." 이런 식으로 집합은 프로그래밍 언어에서 타입과 비슷한데, 특정 타입의 모든 값은 일정한 특징을 공유하기 때문이다.

Go는 정적 타입 프로그래밍 언어다. 이는 변수가 항상 특정 타입을 지니고 있고 해당 타입은 변경될 수 없다는 의미다. 정적 타입 체계는 처음에는 조금 성가셔 보일 수도 있다. 프로그램이 마침내 컴파일되기까지 프로그램을 수정하는 과정에서 상당한 양의 시간을 보내게 될 것이다. 하지만 타입은 프로그램이 하려는 일을 추론하고 흔히 저지르곤 하는 매우 다양한 실수를 잡아내는 데 도움될 것이다.

Go에는 이제부터 좀 더 자세히 살펴볼 내장 데이터 타입이 다수 포함돼 있다.

3.1 숫자

Go에서는 숫자를 표현하는 여러 다양한 타입을 제공한다. 일반적으로 숫자는 정수와 부동 소수점 수라는 두 가지 종류로 나눈다.

정수

정수(수학에도 있는 정수와 마찬가지로)는 소수부가 없는 숫자다(..., -3, -2, -1, 0, 1, ...). 우리가 숫자를 표현하는 데 사용하는 10진수 체계와 달리 컴퓨터는 2진수 체계를 사용한다.

우리의 숫자 체계는 10가지 숫자로 구성돼 있다. 사용 가능한 자릿수가 부족해지면 두(그다음으로 셋, 넷, 다섯) 자릿수를 옆으로 계속 이어지게 해서 큰 수를 표현한다. 예를 들어, 9 다음 숫자는 10이고, 99 다음 숫자는 100과 같은 식으로 이어진다. 컴퓨터도 동일한 과정을 밟지만 10개의 숫자 대신 2개의 숫자만 이용한다. 따라서 0, 1, 10, 11, 100, 101, 110, 111과 같은 식으로 숫자를 센다. 우리가 사용하는 것과 컴퓨터가 사용하는 숫자 체계 간의 또 다른 차이점으로는 모든 정수 정수 타입의 크기가 정해져 있다는 것이다. 즉, 각 숫자마다 정해진 수의 공간만을 차지한다. 따라서 4비트 정수는 0000, 0001, 0010, 0011, 0100과 같을 것이다. 결국 정해진 공간을 다 써버리면 컴퓨터는 맨 처음으로 되돌아간다(이 경우 프로그램이 매우 이상하게 동작할 수 있다).

Go의 정수 타입으로는 uint8, uint16, uint32, uint64, int8, int16, int32, int64가 있다. 8, 16, 32, 64는 각 타입이 사용하는 비트의 수를 나타낸다. uint는 "부호가 없는 정수(unsigned integer)"를 나타내고, int는 "부호가 있는 정수(signed integer)"를 나타낸다. 부호가 없는 정수에는 양수(또는 0)만 담긴다. 더불어 두 개의 별칭 타입도 있는데, uint8과 같은 byteuint32와 같은 rune이 있다. 바이트는 컴퓨터에서 굉장히 흔히 사용되는 측정 단위이므로(1바이트 = 8비트, 1024바이트 = 1킬로바이트, 1024킬로바이트 = 1메가바이트, ...) Go의 byte 데이터 타입은 다른 타입을 정의하는 데 자주 사용되기도 한다. uint, int, uintptr이라고 하는 장비에 의존적인 정수 타입도 있다. 이러한 데이터 타입은 현재 사용 중인 아키텍처의 유형에 따라 크기가 달라지기 때문에 장비에 의존적이다.

일반적으로 정수를 이용할 경우 int 타입을 사용하면 될 것이다.

부동 소수점 수

부동 소수점 수(floating point number)는 소수부가 포함된 숫자(실수, 예: 1.234, 123.4, 0.00001234, 12340000)다. 컴퓨터에서 부동 소수점 수가 실제로 표현되는 방식은 상당히 복잡하며 세세한 표현 방식까지 알 필요는 없다. 따라서 지금은 다음과 같은 사항만 염두에 두면 된다.

  1. 부동 소수점 수는 부정확하다. 때때로 숫자를 표현하는 것이 불가능한 경우도 있다. 예를 들어, 1.01 - 0.99를 계산하면 0.020000000000000018(우리가 예상한 숫자에 굉장히 근접한 숫자이지만 정확히 같은 숫자는 아니다)가 결과로 나타난다.

  2. 정수와 달리 부동 소수점 수는 일정한 크기(32비트나 64비트)가 있다. 크기가 큰 부동 소수점 수를 사용할수록 숫자의 정확도가 높아진다(표현 가능한 자릿수에 따라).

  3. 숫자와 더불어 다른 여러 값도 표현할 수 있는데, 가령 "숫자가 아님"(0/0과 같은 것을 표현하는 데 사용되는 NaN)이나 양의 무한대 및 음의 무한대(+∞ and −∞) 같은 것이 있다.

Go에는 float32float64라는 두 가지 부동 소수점 타입(각각 단정도 부동 소수점 수와 배정도 부동 소수점 수라고도 한다)을 비롯해 complex64complex128이라고 하는 복소수(허수부가 있는 숫자)를 나타내는 두 가지 타입이 있다. 일반적으로 여기서는 부동 소수점 수를 사용할 때 float64를 사용할 것이다.

예제

숫자를 사용하는 예제 프로그램을 작성해 보자. 먼저 chapter3이라는 폴더를 만들고 다음과 같은 내용이 담긴 main.go 파일을 만든다.

package main

import "fmt"

func main() {
    fmt.Println("1 + 1 =", 1 + 1)
}

프로그램을 실행하면 다음과 같은 결과가 출력된다.

$ go run main.go
1 + 1 = 2

이 프로그램은 2장에서 작성한 프로그램과 매우 비슷하다. 똑같이 패키지 선언과 임포트 선언이 있고 함수 선언과 Println 함수를 사용한다는 점까지 동일하다. 그런데 이번에는 Hello World라는 문자열을 출력하는 대신 1 + 1 =이라는 문자열에 이어 1 + 1이라는 수식의 결과를 출력한다. 이 수식은 세 부분으로 구성돼 있다. 숫자 리터럴인 1(int 타입에 해당)과 + 연산자(덧셈을 나타내는), 그리고 또 다른 숫자 리터럴인 1로 구성돼 있다. 이번에는 부동 소수점 수를 이용해 같은 작업을 수행해 보자.

fmt.Println("1 + 1 =", 1.0 + 1.0)

여기서는 .0을 사용해 Go가 이것이 정수가 아니라 부동 소수점 수라는 사실을 알게 했다. 이 프로그램을 실행하면 이전과 동일한 결과가 출력될 것이다.

Go에는 덧셈뿐 아니라 다른 여러 연산자도 있다.

연산자 연산
+ 덧셈
- 뺄셈
* 곱셈
/ 나눗셈
% 나머지

3.2 문자열

2장에서 본 것과 마찬가지로 문자열은 텍스트를 표현하는 데 사용되는 길이가 정해진 문자의 나열이다. Go 언어의 문자열은 개별 바이트로 구성돼 있으며, 보통 각 문자마다 한 바이트를 차지한다(중국어 같은 다른 언어의 문자는 한 바이트 이상으로 표현된다).

문자열 리터럴은 "Hello World"처럼 큰 따옴표를 이용하거나 Hello World처럼 역따옴표를 이용해 생성할 수 있다. 이러한 두 방법의 차이점은 큰따옴표로 만든 문자열은 줄바꿈을 포함할 수 없고 특별한 이스케이프 문자열을 사용할 수 있다는 점이다. 예를 들어, \n은 줄바꿈으로 대체되고 \t는 탭 문자로 대체된다.

문자열에 대해 자주 사용되는 연산으로는 문자열의 길이를 구하거나(len("Hello World")) 문자열 내의 각 문자에 접근하거나("Hello World"[1]) 두 문자열을 하나로 합치는("Hello " + "World") 것이 있다. 앞에서 만든 프로그램을 수정해 이를 시험해 보자.

package main

import "fmt"

func main() {
    fmt.Println(len("Hello World"))
    fmt.Println("Hello World"[1])
    fmt.Println("Hello " + "World")
}

여기서 몇 가지 알아야 할 사항은 다음과 같다.

  1. 공백도 하나의 문자로 간주되므로 문자열의 길이는 10이 아니라 11이며, 세 번째 줄은 "Hello"가 아니라 "Hello "다.

  2. 문자열에는 1이 아닌 0부터 시작하는 "인덱스"가 지정돼 있다. [1]은 첫 번째 요소가 아닌 두 번째 요소를 반환한다. 아울러 프로그램을 실행하면 e이 아닌 101이 출력된다는 점을 눈여겨보자. 이것은 문자가 바이트로 표현되기 때문이다(바이트는 정수라는 사실을 기억하자). 인덱스에 관해 생각하는 한 가지 방법은 문자열을 "Hello World"₁이라고 보여주는 것이다. 이것은 "문자열 Hello World sub 1"이나 "문자열 Hello World at 1" 또는 "문자열 Hello World의 두 번째 문자"로 읽을 것이다.

  3. 문자열 연결은 덧셈과 같은 기호를 쓴다. Go 컴파일러는 인자의 타입을 토대로 어떻게 처리할지 판단한다. +의 양측이 모두 문자열이므로 컴파일러는 덧셈이 아닌 문자열 연결을 의도한다고 가정한다(문자열을 더하는 것은 무의미한 일이다).

3.3 불린

불린 값(조지 불의 이름에서 따온)은 참(true)과 거짓(false)를 나타내는 데 사용되는 특별한 1비트 정수 타입이다(또는 켜짐(on)과 꺼짐(off)를 나타내기도 한다). 불린 값에 대해서는 세 가지 논리 연산자가 사용된다.

연산자 의미
&& and
|| or
! not

다음은 불린 연산자를 어떻게 사용할 수 있는지 보여주는 프로그램이다.

func main() {
    fmt.Println(true && true)
    fmt.Println(true && false)
    fmt.Println(true || true)
    fmt.Println(true || false)
    fmt.Println(!true)
}

이 프로그램을 실행하면 다음과 같이 출력될 것이다.

$ go run main.go
true
false
true
true
false

보통 다음과 같은 진리표(true table)을 이용해 이러한 연산자의 동작 방식을 정의한다.

수식
true && true true
true && false false
false && true false
false && false false
수식
true || true true
true || false true
false || true true
false || false false
수식
!true false
!false true

불린은 Go에 포함된 가장 단순한 값이며, 나중에 나올 타입의 기반을 형성한다.

연습 문제

  1. 정수는 컴퓨터에 어떻게 저장되는가?
  2. 우리는 (십진수 체계에서) 가장 큰 한 자리 숫자는 9이고, 가장 큰 두 자리 숫자는 99라는 사실을 알고 있다. 이진수에서 가장 큰 두 자리 숫자는 11(3)이고, 가장 큰 세 자리 숫자는 111(7)이며, 가장 큰 네 자리 숫자는 1111(15)라는 사실을 고려했을 때 가장 큰 8자리 숫자는 무엇인가? (힌트: 10¹-1 = 9이고 10²-1 = 99다)
  3. 작업에 비해 조금 과도한 감이 있긴 하지만 Go를 계산기로도 쓸 수 있다. 321325 × 424521를 계산해서 그 결과를 터미널에 출력하는 프로그램을 작성하라. (곱셈을 위해 * 연산자를 사용한다)
  4. 문자열이란 무엇인가? 문자열의 길이를 구하는 방법은 무엇인가?
  5. 다음 수식의 값은 무엇인가? (true && false) || (false && true) || !(false && false)
← 이전다음 →