상세 컨텐츠

본문 제목

6장 연산자

Programming/Go입문

by 크리두 2021. 5. 12. 09:20

본문

반응형

6.1 산술연산자

산술 연산자자는 말 그대로 숫자를 연산하는 연산자이다. 종류에는 사칙 연산, 비트 연산, 시프트 연산이 포함된다. 다른 언어랑 마찬가지로 기본적인 사칙 연산을 지원하는건 기본이고, 비트 연산 (and, or, xor 등)을 지원한다. 마지막으로 시프트 연산도 지원한다.

연산자기능설명 (피연산자 타입)
+덧셈두 값을 더함 (정수, 실수, 복소수, 문자열)
-뺄셈두 값을 차이를 구함 (정수, 실수, 복소수)
*곱셈두 값을 곱함 (정수, 실수, 복소수)
/나눗셈두 값을 나눔 (정수, 실수, 복소수)
%나머지두 값을 나눈 후 나머지를 구함 (정수)
&AND 비트 연산두 값을 비트 단위로 AND 연산 (정수)
|OR 비트 연산두 값을 비트 단위로 OR 연산 (정수)
^XOR 비트 연산두 값을 비트 단위로 XOR 연산 (정수)
&^비트 클리어두 값을 비트 단위로 AND NOT 연산 (정수)
<<왼쪽 시프트현재 값의 비트를 특정 횟수만큼 왼쪽으로 이동 (정수 << 양의 정수)
>>오른쪽 시프트현재 값의 비트를 특정 횟수만큼 오른쪽으로 이동 (정수 >> 양의 정수)

피연산자는 3+4에서 +는 연산자이고, + 연산자를 처리하는 대상인 3,4를 피연산자라고 한다. 나머지 연산자는 암호학, 암호화폐에서 사용하는 중요한 연산자이다. 비트 연산자는 크게 3가지 &, |, ^로 볼수 있고, 비트클리어(&^)는 AND 연산자와 XOR 연산자를 조합하는 것이다.

6.1.1 연산의 결과 타입

앞에서도 설명하였듯이 Go 언어는 최강타입 언어이다. 그러므로 모든 연산자의 각 항의 타입은 항상 같아야 가능하다(시프트 연산은 제외). 타입이 다른 경우 타입 변환을 통해서 같게 만들어 연산을 해야 한다. 또한 연산의 결과 타입도 인수 타입과 같게 나온다. 말하자면 정수 타입과 정수 타입을 더하면 같은 정수 타입, 실수와 실수를 연산하면 실수 타입 반환이 된다는 의미이다. 나머지 연산만 정수 타입만 가능하다.

package main

import "fmt"

func main() {
    var x int32 = 7 // ❶
    var y int32 = 3 // ❷

    var s float32 = 3.14 // ❸
    var t float32 = 5

    fmt.Println("x + y = ", x+y)
    fmt.Println("x - y = ", x-y)
    fmt.Println("x * y = ", x*y)
    fmt.Println("x / y = ", x/y) // ❹ ❸
    fmt.Println("x % y = ", x%y) // ➎ ➍

    fmt.Println("s * t = ", s*t)
    fmt.Println("s / t = ", s/t) // ➏
}

6.1.2 비트 연산자

& (AND 연산자)
ABA&B
000
100
010
111

각 비트 별로 양쪽 모두 1인 비트만 1이 되는 것을 말한다.

| (OR 연산자)
ABA|B
000
101
011
111
^ (XOR 연산자)
ABA^B
000
101
011
110

A와 B가 다르면 1이 되는 연산자이다. 다른 언어에서는 ^ 연산자를 승수로 사용되기도 하지만, go 언어에서 승수를 사용하려면 math.Pow라는 함수를 사용해야 가능하다.

&^ (비트 클리어 연산자)

특정 비트를 0으로 바꾸는 연산자이다. 우변값에 해당하는 비트를 클리어하는 연산자이다. ^(XOR) 연산자를 먼저 수행한 후 & 연사자를 수행한다.

열거값 (bit flag)의 특정 비트 값을 0으로 바꾸고 싶을 때 사용한다.

6.1.3 시프트 연산자

시프트 연산자는 왼쪽 (<<), 오른쪽(>>) 연산자이다.

<< (왼쪽 시프트)

비트 값을 비트 단위로 왼쪽으로 이동을 하는 것을 말한다. 예를 들면 10 << 2 = 40이 된다. 왼쪽으로 밀면 값이 2개 밀면 4배, 1개 밀면 2배가 되는데 항상 그런것은 아니다. 1바이트 정수일 경우 왼쪽으로 시프트 할 경우 오버플로우가 난다. 아래 예제를 보면서 확인해보자.

package main

import "fmt"

func main() {
    var x int8 = 4  // ❶ 8비트 정수
    var y int8 = 64 // ❷ 8비트 정수

    fmt.Printf("x:%08b x<<2: %08b x<<2: %d\n", x, x<<2, x<<2) // ❸ 왼쪽 시프트
    fmt.Printf("y:%08b y<<2: %08b y<<2: %d\n", y, y<<2, y<<2) // ➍ 왼쪽 시프트
}
x:00000100 x<<2: 00010000 x<<2: 16
y:01000000 y<<2: 00000000 y<<2: 0

❸ %08b는 8자리로 나타내고 b(binary)는 2진수로 나타내라는 의미이다.

➍ y는 64를 2칸 왼쪽으로 시프트하라는 의미인데, 1바이트 값(127)을 넘어가므로 제대로 된 결과를 얻지 못한다.

>> (오른쪽 시프트)

비트 값을 비트 단위로 오른쪽으로 이동하는 것을 말한다. 이동하는 비트 수는 피연사자는 반드시 양의 정수여야 한다. 즉 부호 있는 정수이면 왼쪽 비트에 부호와 같은 값, 부호없는 정수이면 0으로 채운다. 음수이면 최상위 비트가 1이므로 1로, 양수이면 0으로 채운다.

아래 예제를 통해 음수, 양수 값에 따라 오른쪽 시프트가 어떻게 되는지 확인할 수 있다.

package main

import "fmt"

func main() {
    var x int8 = 16   // ❶ 부호가 있는 정수, 부호 비트값이 0인 수
    var y int8 = -128 // ❷ 부호가 있는 정수, 부호 비트값이 1인 수
    var z int8 = -1   // ❸ 모든 비트값이 1인 정수
    var w uint8 = 128 // ➍ 부호 없는 정수, 최상위 비트값이 1인 양수

    fmt.Printf("x:%08b x>>2: %08b x>>2: %d\n", x, x>>2, x>>2)               // ➎ 오른쪽 시프트❶
    fmt.Printf("y:%08b y>>2: %08b y>>2: %d\n", uint8(y), uint8(y>>2), y>>2) // ➏ 오른쪽 시프트❷
    fmt.Printf("z:%08b z>>2: %08b z>>2: %d\n", uint8(z), uint8(z>>2), z>>2) // ➐ 오른쪽 시프트❸
    fmt.Printf("w:%08b w>>2: %08b w>>2: %d\n", uint8(w), uint8(w>>2), w>>2) // ➑ 오른쪽 시프트❹
}
x:00010000 x>>2: 00000100 x>>2: 4
y:10000000 y>>2: 11100000 y>>2: -32
z:11111111 z>>2: 11111111 z>>2: -1
w:10000000 w>>2: 00100000 w>>2: 32

➐ 오른쪽으로 2칸 밀지만 비워진 값이 1로 채워져 다시 같아진다.

➑ 부호 없는 정수는 2칸 밀면 0이 채워지면서 32가 된다.

Summary

왼쪽 시프트(<<)는 밀어서 비는 자리는 0으로 채우게 되고 2배가 되는 효과 (항상 그렇지는 않다.)

오른쪽 시프트(>>)는 부호에 따라서 다르게 채워지고 4분의1이 되는 효과(항상 그렇지는 않다.)

6.2 비교 연산자

양변을 비교해서 조건에 만족하는지 boolean 값 true, false를 반환하는 연산자이다.

연산자설명반환값
==같다참이면 true
!=다르다거짓이면 false
<작다 
>크다 
<=작거나 크다 
>=크거나 같다 

6.2.1 정수 오버플로

정수 타입의 범위를 벗어날 경우 비정상적인 값이 출력되는 현상을 오버플로(overflow)라고 한다. 그래서 항상 x < x+1을 만족(true)하지 못하는 경우가 생긴다. 아래 예제를 통해 알아본다.

package main

import "fmt"

func main() {
    var x int8 = 127 // ❶ 8비트 부호가 있는 정수 최댓값

    fmt.Printf("%d < %d + 1: %v\n", x, x, x < x+1) // ❷ 비교 연산 수행
    fmt.Printf("x\t= %4d, %08b\n", x, x)           // ❸ x값 확인
    fmt.Printf("x + 1\t= %4d, %08b\n", x+1, x+1)   // ➍ x + 1값 확인
    fmt.Printf("x + 2\t= %4d, %08b\n", x+2, x+2)   // ➎ x + 2값 확인
    fmt.Printf("x + 3\t= %4d, %08b\n", x+3, x+3)   // ➏ x + 3값 확인

    var y int8 = -128                              // 8비트 부호있는 정수 최솟값
    fmt.Printf("%d > %d - 1: %v\n", y, y, y > y-1) // ➐ 비교 연산 수행
    fmt.Printf("y\t= %4d, %08b\n", y, y)           // ➑ y값 확인
    fmt.Printf("y - 1\t= %4d, %08b\n", y-1, y-1)   // ➒ y - 1값 확인
}
127 < 127 + 1: false
x        = 127, 01111111
x + 1    = -128, -10000000
x + 2    = -127, -11111111
x + 3    = 

6.2.2 정수 언더플로

6.2.3 float 비교 연산

수학에서는 0.1+0.2=0.3으로 같지만 컴퓨터에서는 같지 않는 경우가 있다. 바로 실수 오차가 발생하는데 단순한 순자라도 2진수 형태로 표현을 하지 못해서 가장 가까운 값으로 표현을 하게 되면서 발생한다.

package main

import "fmt"

func main() {
    var a float64 = 0.1
    var b float64 = 0.2
    var c float64 = 0.3

    fmt.Printf("%f + %f == %f : %v\n", a, b, c, a+b == c) // ❶
    fmt.Println(a + b)                                    // ❷
}
0.100000 + 0.200000 == 0.300000 : false
0.300000000000000004

0.3이라는 것을 정확히 표현을 하지 못하므로 생기는 문제이다. 이러한 오차를 극복하기 위해서는 어떻게 해야할까?

6.3 실수 오차

6.3.2 오차를 없애는 더 나은 방법

Nexafter라는 함수를 이용하여 오차를 없애는 방법이 있다.

package main

import (
    "fmt"
    "math"
)

func equal(a, b float64) bool {
    return math.Nextafter(a, b) == b // ❶ Nextafter() 로 값을 비교합니다.
}

func main() {
    var a float64 = 0.1
    var b float64 = 0.2
    var c float64 = 0.3

    fmt.Printf("%0.18f + %0.18f = %0.18f\n", a, b, a+b)
    fmt.Printf("%0.18f == %0.18f : %v\n", c, a+b, equal(a+b, c)) // ❷

    a = 0.0000000000004 // ❸ 매우 작은 값으로 변경
    b = 0.0000000000002
    c = 0.0000000000007

    fmt.Printf("%g == %g : %v\n", c, a+b, equal(a+b, c))
}
0.100000000000000006 + 0.200000000000000011 = 0.300000000000000044
0.299999999999999989 == 0.300000000000000044 : true
7e-13 == 6.000000000000001e-13 : false

Ref. Tucker의 Go 언어 프로그래밍 - 공봉식
『Tucker의 Go 언어 프로그래밍』 스터디 요약

반응형

'Programming > Go입문' 카테고리의 다른 글

7장 함수  (0) 2021.05.16

관련글 더보기

댓글 영역