[Go] 가볍게 Go 입문하기 ❷ – 변수

Go 입문자를 위한 가볍게 Go 입문하기를 준비했습니다. Go 언어 기본 문법을 알아봅시다. 기초가 튼튼해야 견고한 건축물을 지을 수 있듯 기본 문법을 제대로 익혀야 훌륭한 코드를 짤 수 있습니다. ‘Hello Go World’, ‘변수’, ‘fmt 패키지를 이용한 텍스트 입출력’, ‘연산자’, ‘함수’까지 총 5편으로 정리했습니다.

변수

변수란 값을 저장하는 메모리 상의 공간입니다. 값에 접근해 값을 변경하는데 사용합니다. 변수는 이름, 값, 타입, 주소 속성을 갖습니다. 변수 간 값의 전달은 항상 복사로 일어납니다. 변수가 갖는 속성과 사용법을 알아봅시다.

 

1. 변수란?

프로그래밍에서 변수(Variable)는 값을 저장하는 메모리 공간을 가리키는 이름입니다.

컴퓨터 입장에서 프로그램은 ‘메모리에 있는 데이터를 언제 어떻게 변경할지를 나타낸 문서’입니다. 따라서 메모리에 있는 데이터 조작은 프로그래밍에 있어 핵심입니다. 변수를 이용하면 쉽고 효과적으로 메모리에 있는 데이터를 조작할 수 있습니다.

 

package main

import "fmt"

func main() {
  var a int = 10 // 1 a 변수 선언
  var msg string = "Hello Variable" // 2 msg 변수 선언

  a = 20 // 3 a값 변경
  msg = "Good Morning" // 4 msg값 변경
  fmt.Println(msg, a) // 5 msg와 a값 출력
}

Good Morning 20

 

❶, ❷에서 각각 a와 msg 변수를 선언했습니다. 변수를 선언한 이후부터 변수를 사용해 값을 저장하거나 변경할 수 있습니다. ❸, ❹에서 각 변숫값을 변경하고 ❺두 변숫값을 출력합니다. 위와 같이 프로그램에서 값을 저장하고 변경하고 사용할 때 변수를 사용합니다. 이제 변수를 더 자세히 알아보겠습니다.

 

2. 변수 선언

변수를 사용하려면 먼저 변수를 선언해야 합니다. 변수 선언은 컴퓨터에게 값을 저장할 공간을 마련하라고 명령을 내리는 겁니다. 이것을 메모리 할당이라고 부릅니다.

 

 

❶ var는 변수의 영문인 variable의 약자로 변수 선언을 알리는 키워드입니다. ❷ 이어서 변수 이름을 적습니다. ❸ 그다음은 타입을 적습니다. ❹ 대입 연산자 = 오른쪽에 초깃값을 적어서 변수 선언을 마칩니다. 변수 선언의 다른 형태에 대해서는 4.4절 ‘변수 선언의 다른 형태’에서 설명합니다.

 

package main

import "fmt"

func main() {
  var minimumWage int = 10 // 1 변수 minimumWage 선언 및 초기화
  var workingHour int = 20 // 2 변수 workingHour 선언 및 초기화

  // 3 변수 income 선언 및 초기화
  var income int = minimumWage * workingHour

  // 변수 minimumWage, workingHour, income 출력
  fmt.Println(minimumWage, workingHour, income)
}

10 20 200

 

❶, ❷ 정수 타입 변수를 선언하고 초기화합니다. 정수 타입 변수 minimumWage와 workingHour를 선언하고 값으로 각각 10과 20을 대입합니다. 컴퓨터는 ❶을 실행할 때 메모리에 정수 타입 데이터를 저장할 공간을 만들고 → minimumWage라고 지칭한 뒤 → 값 10을 복사합니다. 이제 minimumWage라는 변수명을 이용해서 해당 공간에 접근할 수 있습니다.

 

 

❸ 정수 타입 변수를 선언하고 수식으로 초기화합니다. income이라는 변수를 만들고 값으로 minimumWage * workingHour 연산 결과를 대입했습니다. 이처럼 대입 연산자 오른쪽 항에는 10 같은 값뿐만 아니라 변수명이나 수식도 사용할 수 있습니다.

 

3. 변수에 대해 더 알아보기

값을 저장하고 조작하는 데 변수를 사용합니다. 앞서 예제에서 보았듯이 변수를 선언하고 사용하는 건 어렵지 않습니다. 하지만 Go 언어를 더 잘 이해하고 예기치 못한 버그 발생 없이 프로그래밍하려면 변수를 잘 알아야 합니다.

 

3.1 변수의 4가지 속성

변수는 다음 4가지 속성을 가집니다. 각 속성을 자세히 알아보겠습니다.

  • 이름 : 프로그래머는 이름을 통해 값이 저장된 메모리 공간에 손쉽게 접근할 수 있습니다.
  • 값 : 변수가 가리키는 메모리 공간에 저장된 값입니다.
  • 주소 : 변수가 저장된 메모리 공간의 시작 주소를 말합니다.
  • 타입 : 변숫값의 형태를 말합니다. 정수 타입, 실수 타입, 문자열 등의 다양한 타입들이 있습 니다. 자료형, 데이터 타입이라고도 합니다. 이 책에서는 타입을 사용합니다.

아래 그림과 같이 변수 a를 선언하면 컴퓨터는 메모리에 int 타입 크기에 해당하는 공간을 할당하고 그 공간이 위치한 메모리 시작 주소를 a로 지칭합니다. 그리고 메모리 공간에 10이라는 값을 복사합니다.

 

 

변수 a값을 대입 연산자 =를 이용해 50으로 바꾸면 a의 메모리 시작 주소부터 int 타입만큼의 공간에 값 50을 복사합니다.

 

3.2 변수는 이름을 가지고 있다

변수 이름을 변수명이라고 합니다. 프로그래머는 변수명을 사용해서 변숫값이 저장된 메모리 공간에 접근하고 수정할 수 있습니다.

Go 언어에서 변수명을 지을 때는 다음과 같은 규칙을 따라야 합니다.

  • 변수명은 문자, _, 숫자를 사용해 지을 수 있지만 첫 글자는 반드시 문자나 _로 시작해야 합니 다(영어뿐 아니라 한글, 한문 같은 다른 언어 문자도 됩니다).
  • _를 제외한 다른 특수문자(space 포함)를 포함할 수 없습니다.

반드시 지켜야 하는 것은 아니지만, 다음과 같은 권장 사항이 있습니다.

  • 변수명은 영문자를 제외한 다른 언어의 문자를 사용하지 않습니다.
  • 변수명에 여러 단어가 이어지면 두 번째 단어부터는 대문자로 시작합니다.
  • 변수명은 되도록 짧게 합니다. 잠시 사용되는 로컬 변수는 한 글자를 권장합니다.
  • 밑줄 _은 일반적으로 사용하지 않습니다. _를 사용하는 경우를 함수와 패키지 부분에서 설명 합니다.

 

💡Tip: 변수 이름은 숫자밖에 모르는 컴퓨터에게는 무의미합니다. 그래서 코드가 기계어로 변환되면 변수명은 모두 사라지고 대신 메모리 주솟값으로 대체됩니다. 예를 들면 a = 3은 기계어로 MOV [0xc00090003] 3과 같은 형태로 변경됩니다. 0xc00090003은 a 변수가 가리키는 메모리 주소입니다.

 

3.3 변수는 타입을 가지고 있다

타입이 왜 필요할까요? 두 가지 이유가 있습니다. 첫 번째로 타입은 공간 크기를 나타냅니다. 변수는 메모리 주소를 가리킵니다. 그런데 메모리 주소는 값이 있는 메모리 시작 주소만을 알려줍니다. 크기를 알아야지 해당 메모리 주소에서 얼만큼 읽을지 결정할 수 있습니다. 타입을 알면 크기를 알 수 있습니다.

 

 

두 번째로는 타입을 알아야 컴퓨터가 데이터를 해석할 수 있습니다. 예를 들어 컴퓨터에 2진수 1000 0000 값은 uint8 타입으로 128, int8 타입으로 -128, float32 타입으로 1.79366203434e-43입니다.

Go 언어는 숫자, 불리언, 문자열, 배열, 슬라이스, 구조체, 포인터, 함수, 인터페이스, 맵, 채널 등의 타입을 제공합니다.

 

숫자 타입

아래는 Go 언어에서 제공하는 숫자 타입 목록입니다. 부호 없는 정수 숫자는 uint, 부호 있는 정수 숫자는 int, 실수는 float으로 나타내고 뒤에 붙는 숫자는 비트 단위를 나타냅니다. 그래서 int16은 16비트 크기를 갖는 부호 있는 정수 타입입니다. 크기를 신경 쓰지 않는 경우 보통 int, float64를 사용합니다.

 

▼ 표 – 타입별 값의 범위

 

💡Tip: 타입 크기가 클수록 표현할 수 있는 값의 범위가 넓지만 그만큼 메모리를 더 차지합니다. 메모리를 절약해 사용해야 하는 경우에는 값의 범위에 딱 맞는 작은 크기의 타입을 사용해야 합니다.

 

그외 타입

  • 불리언boolean : 참과 거짓 두 값만 가지는 타입입니다. bool로 선언하고 참은 true, 거짓은 false로 씁니다.
  • 문자열 : 문자열 타입의 키워드는 string입니다.
  • 배열array : 같은 타입의 요소들로 이루어진 연속된 메모리 공간을 나타내는 자료구조입니다.
  • 슬라이스 : Go 언어에서 제공하는 가변 길이 배열을 말합니다. 배열은 고정 길이로써 한 번 길 이가 정해지면 늘리거나 줄일 수 없는 반면 슬라이스는 길이를 늘리거나 줄일 수 있습니다.
  • 구조체 : 필드(변수)의 집합 자료구조입니다. 보통 상관관계가 있는 데이터를 묶어놓을 때 사 용합니다. 예를 들어 회원 구조체는 회원ID, 회원명, 주소 등의 필드로 구성할 수 있습니다.
  • 포인터 : 메모리 주소를 값으로 갖는 타입입니다. 포인터를 이용해서 같은 메모리 공간을 가리 키는 여러 변수를 만들 수 있습니다.
  • 함수 타입 : 함수를 가리키는 타입입니다. 다른 말로 함수 포인터라고 말합니다. 사용할 함수를 동적8으로 바꿀 때 유용합니다.
  • 인터페이스 : 메서드 정의의 집합입니다.
  • 맵 : 키key와 값value을 갖는 데이터를 저장해둔 자료구조입니다. 키를 사용해 데이터를 찾는 데 특화된 자료구조입니다. 쉽게 전화번호부나 사전을 생각하시면 됩니다.
  • 채널channel : 멀티스레드9 환경에 특화된 큐 형태 자료구조입니다.

Go 언어가 지원하는 타입이 다양해서 복잡하게 느껴질 수 있을 겁니다. 앞으로 각 타입에 대해 자세히 배우게 되니까 이런 게 있구나 정도로 알고 넘어가도 됩니다.

 

4. 변수 선언의 다른 형태

Go 언어에서는 프로그래머의 편의를 위해서 여러 형태의 변수 선언을 지원하고 있습니다. 다양한 선언 형태에 대해서 알아봅니다.

 

package main

import "fmt"

func main() {
  var a int = 3 // 기본 형태
  var b int // 초깃값 생략. 초깃값은 타입별 기본값으로 대체
  var c = 4 // 타입 생략. 변수 타입은 우변 값의 타입이 됨
  d. := 5 // 선언 대입문 :=을 사용해서 var 키워드와 타입 생략

  fmt.Println(a, b, c, d)
}

3 0 4 5

 

타입별 기본값

변수를 선언할 때 초깃값을 생략하면 다음과 같은 기본값이 자동 대입됩니다.

 

▼ 표 – 타입별 기본값

 

숫자값 기본 타입

타입을 생략하면 우변의 타입으로 좌변(변수)의 타입이 지정됩니다. 만약 우변이 숫자이면 기본 타입으로 결정됩니다. 정수는 int, 실수는 float64가 기본 타입입니다.

 

선언 대입문 :=

선언 대입문이란 말 그대로 선언과 대입을 한꺼번에 하는 구문입니다. 선언 대입문을 사용하면 var 키워드와 타입을 생략해 변수를 선언할 수 있습니다.

 

var b = 3.1415 // b는 float64 타입으로 자동 지정됩니다.
c := 365 // c는 int 타입으로 자동 지정됩니다.
s := "hello world" // s는 string 타입으로 자동 지정됩니다.

 

5. 타입 변환

프로그래밍 언어를 구분할 때 타입 검사를 하는가 안 하는가에 따라 강 타입 언어와 약 타입 언어로 나눕니다. Go 언어는 강 타입 언어 중에서도 가장 강하게 타입 검사를 하는 최강 타입 언어입니다.

Go 언어에서는 연산이나 대입에서 타입이 다르면 에러가 발생합니다.

 

a := 3                      // int
var b float64 = 3.5         // float64

var c int = b     // Error - float64 변수를 int에 대입 불가
d := a * b        // Error - 다른 타입인 int 변수와 float64 연산 불가

var e int64 = 7
f := a * e        // Error - a는 int 타입, e는 int64 타입으로 같은 정수값이지만
                  // 타입이 달라서 연산 불가

var g int = b * 3 // Error - 실수가 정수로 자동으로 바뀌지 않습니다.

 

같은 숫자값이라도 타입이 다르면 연산이 안 되기 때문에 타입을 변환해서 연산을 해줘야 합니다. 이것을 타입 변환(Type Conversion), 형변환이라고 합니다. 타입 변환은 원하는 타입명을 적고 ( )로 변화시키고 싶은 변수를 묶어줍니다.

타입 변환을 이용해서 위 예제를 에러 없이 다시 써보면 다음과 같습니다.

 

package main

import "fmt"

func main() {
  a := 3                // int
  var b float64 = 3.5   // float64

  var c int = int(b)    // ❶ float64에서 int로 변환
  d := float64(a * c)   // int에서 float64로 변환

  var e int64 = 7
  f := int64(d) * e      // float64에서 int64로 변환

  var g int = int(b * 3) // ❷ float64에서 int로 변환
  var h int = int(b) * 3 // ❸ float64에서 int로 변환. g와 값이 다릅니다.
  fmt.Println(g, h, f)
}

10 9 63

 

타입 변환 시 두 가지 유의점이 있습니다. 첫째, 실수 타입에서 정수 타입으로 타입 변환하면 소수점 이하 숫자가 없어진다는 점입니다. 그래서 ❶ c값은 3.5에서 소수점 이하 숫자가 사라진 3이 됩니다. 소수점 이하 숫자는 반올림되지 않고 버려집니다. 그래서 ❷와 ❸ 결과가 서로 달라집니다. ❷에서는 3.5 * 3이 먼저 계산되고 int 타입으로 변환되어서 g값이 10이 되는 반면, ❸에서는 3.5가 먼저 int 타입으로 변환되어 3에 3을 곱한 결과인 9가 됩니다.

둘째, 큰 범위를 갖는 타입에서 작은 범위를 갖는 타입으로 변환하면 값이 달라질 수 있다는 겁니다. 아래 예제를 보겠습니다.

 

package main

import "fmt"

func main() {
  var a int16 = 3456
  var c int8 = int8(a) // ❶ int16 타입에서 int8 타입으로 변환

  fmt.Println(a)
  fmt.Println(c)       // ❷ int8타입인 c값 출력
}

3456
-128

 

❶ 타입 변환을 했더니 c값이 3456에서 ❷ -128로 변했습니다. 2바이트 정수 int16에서 1바이트 정수 int8로 변환할 때 상위 1바이트가 없어지기 때문입니다.

 

 

타입 변환 시 이 두 가지를 항상 주의해야 합니다. 숫자 타입이 아닌 타입들의 타입 변환에 대해서는 각 타입을 다루는 장에서 설명하겠습니다.

 

6. 변수의 범위

변수는 자신이 속한 중괄호 { } 범위를 벗어나면 사라집니다. 범위 예제를 살펴봅시다.

 

package main

import "fmt"

var g int = 10   // ❶ 패키지 전역 변수 선언

func main() {
  var m int = 20 // ❷ 지역 변수 선언

  {
    var s int = 50  // ❸ 지역 변수 선언
    fmt.Println(m, s, g)
  } // ❹ s 지역 변수는 사라짐
  
  m = s + 20 // 5 Error
} // ❻ main 함수 끝

./ex4.6.go:16:7: undefined: s

 

예제에서 어떤 중괄호에도 속해 있지 않은 ❶ g 변수는 패키지 전역 변수(Global Variable, 글로벌 변수)로 같은 패키지 내에서는 어디서나 접근할 수 있습니다. ❷ m 변수는 main() 함수에 속해 있는 지역 변수(Local Variable, 로컬 변수)로 선언 이후부터 main() 함수가 끝나는 ❻ 부분까지 접근할 수 있습니다. ❸ s 변수는 속해 있는 중괄호 { }가 끝나는 ❹ 지점에서 사라집니다. ❺ 에서 변수가 사라졌기 때문에 에러가 발생합니다.

 

7. 숫자 표현

음수나 실수 같은 숫자를 메모리에 표현하는 방법을 알아봅시다.

 

7.1 정수 표현

부호 있는 정수 타입을 표현하는 방법을 살펴보겠습니다. 부호를 표현해야 하기 때문에 첫 번째 비트를 부호 비트로 정해서 1이면 음수를, 0이면 양수를 나타냅니다. 2바이트 부호 있는 정수에서 양수 15는 다음과 같이 표현됩니다.

 

 

숫자 15를 2진수로 변환하면 1111(23×1+22×1+21×1+20×1)이기 때문입니다. 그럼 음수 15는 어떻게 표현할까요? 양수 15에서 부호 비트를 1로 바꿔서 다음과 같이 표현할까요?

 

 

아쉽게도 그렇지 않습니다. 보수로 표현하게 됩니다.

왜냐하면 단순히 최상위 비트가 1이냐 0으로 판단을 하게 되면 +0과 -0이라는 이상한 개념이 생깁니다(0에 플러스와 마이너스는 의미가 없죠).

 

 

게다가 표현할 수 있는 숫자가 하나 줄어서 낭비가 발생하기 때문에 컴퓨터에서 음수를 절댓값의 2의 보수로 표현합니다. 2의 보수를 만드는 방법은 모든 비트의 0을 1로, 1을 0으로 바꾸고 나서, , 1을 더하면 됩니다.

2의 보수를 적용해 -15를 표현해봅시다.

 

 

또한 2의 보수로 음수로 표현하면 음수를 별도 처리 없이 바로 더할 수 있는 장점이 있습니다.

 

 

7.2 실수의 표현

소수점이 있는 실수는 1과 2 사이에도 무수히 많은 숫자가 있기 때문에 정수 타입처럼 바로 2진수로 변환해서 사용할 수가 없습니다. 그래서 실수를 표현하는 데 특수한 방법을 적용합니다. Go언어는 IEEE-754 표준을 따라 실수를 표현합니다. 예를 들어 1024.234는 0.1024234×104으로 나타낼 수 있고 0.1024234e+04라고 쓰기도 합니다. 이때 1024234가 소수부이고 10의 승수인 4가 지수부입니다. 컴퓨터에서 실수는 이렇게 소수부와 지수부를 나눠서 표현합니다.

4바이트 실수에서는 제일 왼쪽 1비트가 부호 비트로 1이면 음수, 0이면 양수입니다. 그뒤 8비트는 지수부를 나타내고 나머지 23비트는 소수부를 나타냅니다.

 

 

중요한 점은 모든 실수를 표현할 수 없고 한계가 있다는 점입니다. 특히 소수부 비트수가 정해져있기 때문에 표현할 수 있는 숫자에 한계가 있습니다.

 

 

실수는 무한히 많은 숫자가 있기 때문에 최솟값, 최댓값보다는 표현할 수 있는 소수부 자릿수에 더 주의를 기울여야 합니다. 일반적으로 float32는 7자리까지 표현하고 float64는 15자리까지 표현할 수 있습니다. 그래서 Go 언어에서 실숫값을 표현할 때 정확한 값이 아닌 타입이 허용하는 범위에서 가장 가까운 근삿값으로 표현하게 됩니다.

이 자릿수를 넘기면 수가 제대로 표현되지 않습니다. 예제를 살펴보겠습니다.

 

package main

import "fmt"

func main() {
  var a float32 = 1234.523
  var b float32 = 3456.123
  var c float32 = a * b // ❶
  var d float32 = c * 3
  
  fmt.Println(a)
  fmt.Println(b)
  fmt.Println(c)
  fmt.Println(d)
}

1234.523
3456.123
4.266663e+06 ❷
1.2799989e+07 ❸

 

❶ 1234.523 × 3456.123의 정확한 결과는 4266663.334329지만 float32의 7자리 제한에 걸려서 ❷ 실제 c값은 4266663이 됩니다. 문제는 여기서 끝나는 게 아니라 연산이 누적될수록 오차가 점점 커집니다. d의 올바른 값은 12799990.002987이지만 ❸ 127999890이 출력됐습니다. 1 미만의 오차가 곱하기 연산 한 번으로 100에 가까운 큰 차이를 만들었습니다. 따라서 회계나 금융처럼 정확한 수치 연산이 필요한 프로그래밍에서는 실수 타입 사용에 각별히 주의해야 합니다.

 

핵심 요약

  1. 변수는 값을 저장하는 메모리 공간입니다. 변수를 사용해서 메모리에 접근하여 값을 조정할 수 있습니다.
  2. 변수를 사용하려면 먼저 선언을 해야 합니다. 변수 선언은 컴퓨터에게 값을 저장할 공간을 메 모리에 마련하라고 명령을 내리는 겁니다. 이를 메모리 할당이라고 합니다.
  3. 변수는 4가지 속성을 가지고 있습니다. 4가지 속성은 이름, 값, 타입, 주소입니다.
  4. 변수 선언 방법은 다양합니다. 편의를 고려해 초깃값을 생략하거나 타입을 생략하는 등 다양 한 선언 방법을 제공합니다.
  5. 타입 변환은 한 타입의 값을 변환 가능한 다른 타입으로 변환시키는 겁니다. Go 언어는 자동 변환을 지원하지 않기 때문에 연산이나 대입에서 타입 변환을 해줘야 합니다.
  6. 숫자 타입은 크기에 따라 표현할 수 있는 값의 범위가 다릅니다. 특히 실수 타입은 유효 자릿 수가 정해져 있어 주의해서 사용해야 합니다.

 

다음편에서 계속 됩니다.

WRITER

공봉식

13년 차 게임 서버 프로그래머로 다양한 장르의 온라인 게임을 개발했습니다. 넥슨과 네오위즈를 거쳐서 현재는 EA 캐나다에서 근무 중입니다. 「Tucker Programming」 유튜브 채널을 운영합니다.

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내