할당 문

할당 문 (Assignment statements)

할당(assignment)은 변수에 저장된 값을 새 값으로 바꾸는 가장 기본적인 동작이에요. 변수 하나에 값 하나를 넣는 단순한 할당부터, 여러 변수에 여러 값을 한 번에 나눠 담는 방식까지 Go에서 할당이 어떻게 동작하는지 하나씩 확인해 볼게요. 그 과정에서 '언제, 어떤 순서로 값이 실제로 대입되는지'도 함께 잡아가요.

출처: Go Specification

본문

할당(assignment)은 변수에 저장된 현재 값을 표현식으로 지정된 새 값으로 대체해요. 할당 문은 하나의 값을 하나의 변수에 배정할 수도 있고, 여러 값을 그에 대응하는 개수의 변수에 배정할 수도 있어요.

<a id="Assignment">Assignment</a> = <a href="#ExpressionList" class="noline">ExpressionList</a> <a href="#assign_op" class="noline">assign_op</a> <a href="#ExpressionList" class="noline">ExpressionList</a> .

<a id="assign_op">assign_op</a>  = [ <a href="#add_op" class="noline">add_op</a> | <a href="#mul_op" class="noline">mul_op</a> ] "=" .

왼쪽 피연산자는 각각 주소 지정이 가능(addressable)해야 하거나, 맵 인덱스 표현식이거나, (= 할당에서만) 빈 식별자(blank identifier)여야 해요. 피연산자는 괄호로 감쌀 수도 있어요.

x = 1
*p = f()
a[i] = 23
(k) = <-ch  // same as: k = <-ch

여기서 잠깐, x op= y 형태의 *할당 연산(assignment operation)*도 있어요. op가 이항 산술 연산자일 때 이 할당 연산은 x = x op (y)와 같지만, x는 한 번만 평가돼요. op=는 하나의 토큰이에요. 할당 연산에서는 왼쪽과 오른쪽 표현식 목록이 정확히 하나의 단일 값 표현식을 담아야 하고, 왼쪽 표현식은 빈 식별자여선 안 돼요.

a[i] <<= 2
i &^= 1<<n

이번엔 튜플 할당(tuple assignment)을 볼게요. 튜플 할당은 다중 값을 반환하는 연산의 각 요소를 변수 목록에 배정해요. 형태는 두 가지가 있는데, 첫 번째는 오른쪽 피연산자가 함수 호출, 채널이나 연산, 타입 단언(type assertion) 같은 단일 다중 값 표현식인 경우예요. 이때 왼쪽 피연산자 개수는 값의 개수와 일치해야 해요. 예를 들어 f가 두 값을 반환하는 함수라면,

x, y = f()

는 첫 번째 값을 x에, 두 번째 값을 y에 배정해요. 두 번째 형태에서는 왼쪽 피연산자 개수가 오른쪽 표현식 개수와 같아야 하고, 오른쪽의 각 표현식은 단일 값이어야 해요. 그러면 오른쪽의 n번째 표현식이 왼쪽의 n번째 피연산자에 배정돼요:

one, two, three = '一', '二', '三'

빈 식별자는 할당에서 오른쪽 값을 무시할 수 있게 해줘요. 평가는 하되 결과는 버리는 자리라고 보면 돼요:

_ = x       // evaluate x but ignore it
x, _ = f()  // evaluate f() but ignore second result value

할당은 두 단계로 진행돼요. 먼저 왼쪽의 인덱스 표현식포인터 역참조(셀렉터 안의 암묵적 포인터 역참조 포함)의 피연산자, 그리고 오른쪽의 표현식들이 모두 일반적인 순서로 평가돼요. 그다음에 할당이 왼쪽에서 오른쪽 순서로 수행돼요. 말로만 들으면 헷갈리기 쉬우니 아래 예시로 순서를 직접 확인해 볼게요:

a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2  // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1  // set x[0] = 2, i = 1

x[0], x[0] = 1, 2  // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5  // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7  // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x {  // set i, x[2] = 0, x[0]
	break
}
// after this loop, i == 0 and x is []int{3, 5, 3}

예시에서 포인트인 부분이 왼쪽에서 오른쪽으로 차례로 수행된다는 점이에요. x[0], x[0] = 1, 2는 먼저 x[0]1을 넣고, 그다음에 같은 x[0]2를 넣어서 결국 2가 되는 식이죠.

할당에서 각 값은 배정받는 피연산자의 타입에 배정 가능(assignable)해야 해요. 다만 다음의 특수한 경우들이 있어요:

  1. 어떤 타입의 값이든 빈 식별자에 배정될 수 있어요.

  2. 타입 없는 상수(untyped constant)가 인터페이스 타입 변수나 빈 식별자에 배정되면, 그 상수는 먼저 암묵적으로 변환되어 기본 타입(default type)이 돼요.

  3. 타입 없는 불리언 값이 인터페이스 타입 변수나 빈 식별자에 배정되면, 먼저 암묵적으로 bool 타입으로 변환돼요.

마지막으로, 할당은 변수에 저장된 데이터만 바꾼다는 점을 기억해 둘게요. 값이 참조(reference)를 담고 있다면, 할당은 참조만 복사하지 그 참조된 데이터(슬라이스의 밑바탕 배열 같은 것)까지 복사하지는 않아요. 그래서 슬라이스나 맵을 다른 변수에 그대로 담으면 같은 데이터를 공유하게 돼요:

var s1 = []int{1, 2, 3}
var s2 = s1                    // s2 stores the slice descriptor of s1
s1 = s1[:1]                    // s1's length is 1 but it still shares its underlying array with s2
s2[0] = 42                     // setting s2[0] changes s1[0] as well
fmt.Println(s1, s2)            // prints [42] [42 2 3]

var m1 = make(map[string]int)
var m2 = m1                    // m2 stores the map descriptor of m1
m1["foo"] = 42                 // setting m1["foo"] changes m2["foo"] as well
fmt.Println(m2["foo"])         // prints 42

더 알아보기

  • Assignability — 어떤 값이 어떤 타입에 배정될 수 있는지의 일반 규칙이에요.
  • Blank identifier — 결과를 버리는 데 쓰는 빈 식별자를 자세히 봐요.
  • Order of evaluation — 할당이 평가되는 순서와 연관된 규칙이에요.
  • Variables — 할당이 바꾸는 대상인 변수의 개념이에요.