Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5장. 연산자

변수에 값을 담았다면 이제 그 값으로 계산을 해야 한다. Go 의 연산자는 C 계열 언어를 써 본 사람에게 거의 익숙하지만, 몇 가지 함정과 특이한 점이 있다.

목표:

  • 산술 / 비교 / 논리 연산자의 동작 방식 익히기
  • 정수 나눗셈과 오버플로우의 함정 이해하기
  • 연산자 우선순위를 큰 그림으로 잡기

5.1 산술 연산자

기본 다섯 가지다.

연산자의미
+덧셈3 + 2 → 5
-뺄셈3 - 2 → 1
*곱셈3 * 2 → 6
/나눗셈7 / 2 → ?
%나머지7 % 2 → 1
a := 10
b := 3

fmt.Println(a + b)  // 13
fmt.Println(a - b)  // 7
fmt.Println(a * b)  // 30
fmt.Println(a / b)  // 3
fmt.Println(a % b)  // 1

정수 나눗셈의 함정

위 표에서 7 / 2 의 답을 3.5 로 생각했다면 함정에 빠진 거다.

정수끼리의 나눗셈은 결과도 정수다. 소수점 아래는 그냥 잘려 나간다.

x := 7 / 2          // x 는 3, 3.5 아님
y := 7.0 / 2.0      // y 는 3.5
z := float64(7) / 2 // z 는 3.5

실수 결과가 필요하면 피연산자 중 하나라도 실수여야 한다.

음수 나머지 동작

% 연산자는 음수를 만나면 직관과 살짝 다를 수 있다. Go 에서는 결과의 부호가 좌변(피제수) 을 따른다.

fmt.Println( 7 %  3)  //  1
fmt.Println(-7 %  3)  // -1
fmt.Println( 7 % -3)  //  1
fmt.Println(-7 % -3)  // -1

수학적으로 “양수 나머지” 를 원할 때는 ((a % b) + b) % b 같은 식이 필요하다.

오버플로우는 wrap-around

정수형은 표현할 수 있는 범위가 정해져 있다. 그 범위를 넘으면 어떻게 될까?

C 처럼 그냥 한 바퀴 돌아 버린다. (wrap-around)

var n int8 = 127
n = n + 1
fmt.Println(n)   // -128

int8 의 최대값은 127이고, 거기에 1 을 더하면 정수가 한 바퀴 돌아 최소값 -128 이 된다.

런타임 에러가 나는 게 아니다. 그냥 조용히 잘못된 값이 생긴다. “수치가 클 수 있다” 싶으면 int64 같은 넉넉한 타입을 쓰자.

부호 / 부호 없음

산술 연산자는 정수와 실수 모두에 쓴다. 하지만 +, -, *, / 만 실수에 쓸 수 있고, % (나머지) 는 정수에만 쓸 수 있다.

fmt.Println(7.5 % 2.0)  // 컴파일 에러

5.2 비교 연산자

두 값을 비교해서 bool (참 / 거짓) 을 돌려준다.

연산자의미
==같다
!=다르다
<작다
>크다
<=작거나 같다
>=크거나 같다
a, b := 3, 5
fmt.Println(a == b)  // false
fmt.Println(a != b)  // true
fmt.Println(a < b)   // true
fmt.Println(a >= b)  // false

문자열도 비교 가능

문자열은 사전식으로 비교된다. 앞에서부터 한 글자씩 코드값을 비교해 가는 방식이다.

fmt.Println("apple" == "apple")  // true
fmt.Println("apple" < "banana")  // true
fmt.Println("apple" < "Banana")  // false ('B' 가 'a' 보다 작음)

대문자가 소문자보다 코드값이 작다는 점에 주의.

서로 다른 타입은 비교 불가

비교 연산자도 같은 타입끼리만 쓸 수 있다.

var a int = 3
var b int64 = 3
fmt.Println(a == b)  // 컴파일 에러

비교하려면 한쪽을 변환해야 한다.

fmt.Println(int64(a) == b)  // true

이 점이 답답해 보일 수 있지만, “의도하지 않은 비교” 로 생기는 버그를 막아 준다.


5.3 논리 연산자

bool 값들끼리 조합할 때 쓴다.

연산자의미
&&그리고 (AND)
||또는 (OR)
!부정 (NOT)
a, b := true, false

fmt.Println(a && b)  // false
fmt.Println(a || b)  // true
fmt.Println(!a)      // false

단축 평가 (short-circuit)

&&|| 는 왼쪽부터 평가하다가 결과가 확정되면 오른쪽은 아예 보지 않는다.

// && 의 단축 평가
//   왼쪽이 false 면 오른쪽은 보지 않는다
if x != 0 && 100/x > 5 {
    ...
}

위 코드에서 x 가 0 이면 x != 0 이 false 라서 오른쪽 100/x 는 실행되지 않는다. 0 으로 나누는 런타임 에러를 막아 준다.

|| 도 비슷하다. 왼쪽이 true 면 오른쪽은 평가하지 않는다.

if name == "" || isInvalid(name) {
    ...
}

이 패턴은 실전에서 매우 자주 쓰인다.


5.4 대입 연산자

기본은 = 다.

x := 10
x = 20      // x 에 20 을 다시 대입

산술과 결합된 형태도 자주 쓴다.

연산자의미풀어 쓰면
+=더해서 대입x = x + y
-=빼서 대입x = x - y
*=곱해서 대입x = x * y
/=나누어서 대입x = x / y
%=나머지로 대입x = x % y
x := 10
x += 5    // x 는 15
x *= 2    // x 는 30
x %= 7    // x 는 2

증감 연산자

x++x-- 도 있다. 하지만 Go 의 증감 연산자는 다른 언어와 결이 다르다.

Go 의 x++, x-- 는 식(expression) 이 아니라 문(statement) 이다.

  • 다른 식 안에 끼워 넣지 못한다
  • 후위 형태만 있다 (전위 ++x 같은 건 없다)
x := 10
x++              // OK. x 는 11
x--              // OK. x 는 10

y := x++         // 컴파일 에러
fmt.Println(x++) // 컴파일 에러

C 같은 언어에서 흔히 보던 a = b++ 같은 영리한 코드는 Go 에선 쓸 수 없다. 이 점이 처음엔 어색하지만, “증감은 한 줄짜리 동작” 이라는 단순한 규칙을 강제해 가독성을 지킨다.


5.5 비트 연산자

비트 단위로 정수를 다루는 연산자다. 처음에는 자주 쓸 일이 없지만 알아 두면 좋다.

연산자의미
&AND (둘 다 1)
|OR (하나라도 1)
^XOR (둘이 다를 때 1)
<<왼쪽 시프트
>>오른쪽 시프트
&^AND NOT (Go 특유)
a := 0b1100  // 12
b := 0b1010  // 10

fmt.Println(a & b)   //  8  (0b1000)
fmt.Println(a | b)   // 14  (0b1110)
fmt.Println(a ^ b)   //  6  (0b0110)
fmt.Println(a << 1)  // 24
fmt.Println(a >> 1)  //  6

&^ (AND NOT) 연산자

다른 언어엔 잘 없는 Go 특유의 연산자다.

a &^ b 는 “a 의 비트 중에서 b 가 1 인 자리만 끄기” 다. a & (^b) 와 같다.

a := 0b1101  // 13
b := 0b0100  //  4

fmt.Println(a &^ b)  // 0b1001 → 9

“플래그 비트를 끄고 싶을 때” 종종 등장한다. 지금은 “이런 게 있다” 정도만 기억하자.


5.6 연산자 우선순위

Go 의 우선순위는 다섯 단계로 단순하게 정리돼 있다. 위쪽일수록 먼저 계산된다.

우선순위연산자
1 (가장 높음)*, /, %, <<, >>, &, &^
2+, -, |, ^
3==, !=, <, <=, >, >=
4&&
5 (가장 낮음)||

수학적인 직관과 대체로 맞다. 곱셈/나눗셈이 덧셈/뺄셈보다 먼저고, 산술이 비교보다 먼저고, AND 가 OR 보다 먼저다.

result := 3 + 4 * 2     // 11 (3 + 8), 14 아님
ok := a > 0 && b > 0    // (a > 0) && (b > 0)

헷갈리면 괄호로

우선순위를 외우려 애쓰기보다, 조금만 복잡하면 그냥 괄호를 치는 게 낫다.

// 동작은 같지만 의도가 더 분명함
result := 3 + (4 * 2)
ok     := (a > 0) && (b > 0)

코드를 읽는 사람도, 미래의 본인도 고마워한다.


5.7 정리

  • 산술 연산자에서 가장 자주 실수하는 곳은 정수 나눗셈(/)
    • 정수 / 정수 = 정수
    • 실수 결과가 필요하면 피연산자를 실수로
  • 비교 연산자는 bool 을 돌려준다, 다른 타입끼리는 비교 불가
  • 논리 연산자(&&, ||)는 단축 평가를 한다
  • x++, x-- 는 문이지 식이 아니다 (후위만 가능)
  • 비트 연산자에는 Go 특유의 &^ (AND NOT) 가 있다
  • 우선순위 외우기 어렵다면 괄호로 명시하자

값과 연산이 갖춰졌다. 다음 장에서는 그동안 가볍게만 다뤘던 문자열을 본격적으로 들여다본다. 한글이 등장하면서 흥미로운 함정이 몇 개 나온다.