현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

Go의 줄 바꿈 규칙

Go 코드를 어느 정도 작성해 보았다면, Go 프로그래밍을 할 때 사용할 수 있는 코드 스타일에 제약이 있다는 것을 알 것입니다. 그중 하나로, 공백 문자가 들어갈 자리에서 줄 바꿈을 하는 것도 Go에서는 아무렇게나 할 수 없습니다. 이번 장에서는 Go에서 줄 바꿈을 할 때의 규칙에 대해서 자세히 알아보도록 하겠습니다.

세미콜론 삽입 규칙

실무에서 우리는 "명시적인 코드 블록을 시작하는 중괄호({) 전에 줄 바꿈을 하지 않는다"라는 규칙을 정하는 경우가 많습니다. 예를 들어, 다음 for 반복문 코드는 컴파일에 실패합니다.
	for i := 5; i > 0; i--
	{ // unexpected newline, expecting { after for clause
	}
이 코드를 컴파일이 되도록 하려면, 다음과 같이 여는 중괄호가 새로운 줄에 있지 않도록 해야 합니다.
	for i := 5; i > 0; i-- {
	}

하지만, 위 규칙에는 예외가 있습니다. 예를 들어, 다음과 같이 for 뒤에 아무것도 붙지 않은 반복문 블록은 정상적으로 컴파일이 가능합니다.
	for
	{
		// do something ...
	}

그렇다면, Go 언어에서의 줄 바꿈에 대한 근본적인 규칙은 무엇일까요? 이 질문에 대답하기 전에 우리는 먼저 Go 코드의 모든 줄의 끝에는 원칙적으로 세미콜론(;)이 있어야 한다는 것을 알아야 합니다. 하지만 실제 Go 코드에서 세미콜론은 거의 사용되지 않습니다. 왜일까요? 바로 대부분의 세미콜론은 필수가 아니며 생략될 수 있기 때문입니다. 이렇게 생략된 세미콜론들은 Go 컴파일러가 자동으로 추가해줍니다.

예를 들어, 다음 프로그램에서 사용된 10개의 세미콜론은 모두 생략할 수 있습니다.
package main;

import "fmt";

func main() {
	var (
		i   int;
		sum int;
	);
	for i < 6 {
		sum += i;
		i++;
	};
	fmt.Println(sum);
};

위 프로그램이 semicolons.go라는 이름의 파일에 저장되어 있을 때, go fmt semicolons.go 명령을 실행하면 해당 파일의 모든 불필요한 세미콜론들이 삭제됩니다. 지워진 세미콜론들은 소스 코드를 컴파일하면서 자동으로 (메모리 안에서) 다시 추가됩니다.

그렇다면 세미콜론은 어떤 규칙을 따라 삽입될까요? Go 언어 명세의 해당 부분을 읽어보도록 합시다.

대부분의 Go 문법에서 공식적으로 세미콜론 ";" 을 종결자로 사용한다. Go 프로그램에서는 아래 두 가지 규칙에 따라 세미콜론 대부분을 생략할 수 있다.

  1. 입력이 토큰으로 해석될 때, 줄의 마지막 언어요소가 다음과 같은 경우, 세미콜론이 자동적으로 줄의 가장 끝 바로 뒤에 붙는다.
    • 식별자
    • 정수, 부동소수, 허수, 룬, 문자열 고정값
    • 예약어 break, continue, fallthrough, return
    • 연산자와 구두점 ++, --, ), ], }
  2. 복합문이 한 줄에 표시될 수 있도록, )} 앞에서는 세미콜론이 생략될 수 있다.

첫 번째 규칙에 열거된 상황의 경우, 당연히 위의 예시와 같이 수동으로 세미콜론을 추가할 수 있습니다. 이 세미콜론들은 선택적으로 사용할 수 있다는 말이죠.

두 번째 규칙은 여러 항목을 선언한 뒤 오는 닫는 괄호 ) 앞과 코드 블록이나 자료형 (struct나 interface) 선언 뒤 오는 닫는 괄호 } 앞의 세미콜론은 선택적이라는 것을 의미합니다. 이 세미콜론들이 없다면, 컴파일러가 자동으로 다시 넣어줍니다.

이 두 번째 규칙은 다음 코드가 우리가 쓸 수 있는 유효한 코드라는 것을 의미합니다.
import (_ "math"; "fmt")
var (a int; b string)
const (M = iota; N)
type (MyInt int; T struct{x bool; y int32})
type I interface{m1(int) int; m2() string}
func f() {print("a"); panic(nil)}
생략한 세미콜론들을 컴파일러가 자동으로 넣어준 뒤에는 다음과 같은 코드가 됩니다.
import (_ "math"; "fmt";);
var (a int; b string;);
const (M = iota; N;);
type (MyInt int; T struct{x bool; y int32;};);
type I interface{m1(int) int; m2() string;};
func f() {print("a"); panic(nil);};

이 두 가지 조건을 제외하고 컴파일러가 세미콜론을 넣어주는 경우는 없습니다. 다른 상황에서는 우리가 직접 필요한 세미콜론을 넣어주어야 하죠. 예를 들어, 위의 예시 코드에서 각 줄의 첫 번째 세미콜론들은 모두 필요합니다. 아래 예시의 세미콜론들 역시 모두 필수로 넣어주어야 하죠.
var a = 1; var b = true
a++; b = !b
print(a); print(b)

세미콜론 삽입 규칙을 다시 살펴보면 우리는 for 키워드 바로 뒤에는 절대 자동으로 세미콜론이 추가되지 않을 것임을 알 수 있습니다. 위의 for 반복문 뒤에 아무것도 붙지 않았던 예시 코드가 유효한 이유죠.

이 세미콜론 추가 규칙에 따라 증감 연산자들은 표현식(expression)으로 사용될 수 없고 반드시 구문(statement)으로만 사용이 가능합니다. 예를 들어, 다음의 코드는 유효하지 않습니다.
func f() {
	a := 0
	// 아래 두 줄 모두 컴파일에 실패합니다.
	println(a++) // unexpected ++, expecting comma or )
	println(a--) // unexpected --, expecting comma or )
}
컴파일러는 위의 코드를 아래와 같이 볼 것이기 때문이죠.
func f() {
	a := 0;
	println(a++;);
	println(a--;);
}

또한 선택자(selector) . 앞에서 줄을 바꿀 수 없는 것도 세미콜론 추가 규칙 때문입니다. 아래 코드와 같이 선택자 뒤에서만 줄 바꿈이 가능하고,
	anObject.
		MethodA().
		MethodB().
		MethodC()
선택자 앞에서 줄 바꿈을 한 아래 코드는 컴파일이 되지 않습니다.
	anObject
		.MethodA()
		.MethodB()
		.MethodC()
두 번째 코드를 컴파일러가 읽으면 규칙에 따라 줄 끝마다 세미콜론을 붙일 것이고, 그렇게 나온 다음과 같은 코드는 당연히 유효하지 않기 때문입니다.
	anObject;
		.MethodA();
		.MethodB();
		.MethodC();

이러한 세미콜론 삽입 규칙은 더 깔끔한 코드를 짜게 도와줍니다. 하지만 가끔은 유효하지만 조금 이상한 코드 역시 쓸 수 있게 해 주죠. 예를 들어봅시다.
package main

import "fmt"

func alwaysFalse() bool {return false}

func main() {
	for
	i := 0
	i < 6
	i++ {
		// use i ...
	}

	if x := alwaysFalse()
	!x {
		// do something ...
	}

	switch alwaysFalse()
	{
	case true: fmt.Println("true")
	case false: fmt.Println("false")
	}
}

위 예시에 나와 있는 세 개의 제어 흐름(control flow) 블록은 모두 유효합니다. 컴파일러가 9, 10, 15, 20번째 줄 끝에 세미콜론을 추가하기 때문이죠.

여기서 주목할 부분은 위 예시의 switch-case 블록은 false가 아닌 true를 출력한다는 점입니다. 아래의 코드와는 정반대의 결과죠.
	switch alwaysFalse() {
	case true: fmt.Println("true")
	case false: fmt.Println("false")
	}
먼저 보았던 줄 바꿈이 들어가 있는 코드를 go fmt 명령으로 정규화하면 alwaysFalse() 함수 호출 뒤에 세미콜론이 다음과 같이 삽입되게 됩니다.
	switch alwaysFalse();
	{
	case true: fmt.Println("true")
	case false: fmt.Println("false")
	}
이는 다음 코드와 동치이므로, true를 출력하게 되는 것이죠.
	switch alwaysFalse(); true {
	case true: fmt.Println("true")
	case false: fmt.Println("false")
	}

따라서 가끔은 go fmtgo vet을 실행해서 코드를 점검해 보는 것이 좋습니다.

흔한 경우는 아니지만 위와 반대로 언뜻 유효해 보이는 코드에 세미콜론 삽입 규칙을 적용하고 보니 유효하지 않은 코드인 것을 알게 되는 경우도 있습니다. 예를 들어, 다음 코드는 컴파일이 되지 않습니다.
func f(x int) {
	switch x {
	case 1:
	{
		goto A
		A: // 정상적으로 컴파일됨
	}
	case 2:
		goto B
		B: // syntax error: missing statement after label
	case 0:
		goto C
		C: // 정상적으로 컴파일됨
	}
}

컴파일 중 출력된 오류 메시지를 보면 라벨의 선언 뒤에는 구문(statement)이 따라와야 한다고 합니다. 하지만 위 코드에서 선언된 세 라벨 모두 뒤에 구문이 있어 보이지는 않는데요. 왜 B:의 선언만 유효하지 않은 걸까요? 바로 아래와 같이 두 번째 규칙에 따라 컴파일러가 A:C: 뒤에 있는 } 앞에 세미콜론을 넣기 때문입니다.
func f(x int) {
	switch x {
	case 1:
	{
		goto A
		A:
	;} // 세미콜론 삽입됨
	case 2:
		goto B
		B: // syntax error: missing statement after label
	case 0:
		goto C
		C:
	;} // 세미콜론 삽입됨
}

단독으로 쓰인 세미콜론은 빈 구문을 구성하는데, 이 또한 하나의 구문이므로 그 앞의 A:C:의 선언 역시 유효하게 되는 것입니다. 반면 B:의 선언 뒤에는 구문이 아닌 case 0:가 있으므로 B:의 선언은 유효하지 않은 것이죠.

B: 라벨 선언 뒤에 세미콜론(빈 구문)을 추가하면 정상적으로 컴파일이 되는 것을 볼 수 있습니다.

쉼표(,)는 자동으로 추가되지 않는다

합성 리터럴이나 함수의 전달인자·매개변수·반환값 목록과 같이 비슷한 항목들을 여러 개 포함하는 몇몇 구문들은 그 항목들을 구분하기 위해 쉼표를 사용합니다. 이때 마지막 항목 뒤에는 항상 쉼표가 따라올 수 있는데요. 만약 이 쉼표가 줄을 바꾸기 전 마지막 문자라면 필수이지만, 그렇지 않은 때는 생략해도 무방합니다. 세미콜론과는 달리 그 어떤 경우에도 컴파일러가 자동으로 쉼표를 넣는 일은 없습니다.

예를 들어, 다음은 유효한 Go 코드입니다.
func f1(a int, b string,) (x bool, y int,) {
	return true, 789
}
var f2 func (a int, b string) (x bool, y int)
var f3 func (a int, b string, // 마지막 쉼표가 필요합니다
) (x bool, y int,             // 마지막 쉼표가 필요합니다
)
var _ = []int{2, 3, 5, 7, 9,} // 마지막 쉼표를 생략할 수 있습니다
var _ = []int{2, 3, 5, 7, 9,  // 마지막 쉼표가 필요합니다
}
var _ = []int{2, 3, 5, 7, 9}
var _, _ = f1(123, "Go",) // 마지막 쉼표를 생략할 수 있습니다
var _, _ = f1(123, "Go",  // 마지막 쉼표가 필요합니다
)
var _, _ = f1(123, "Go")
// The same for explicit conversions.
var _ = string(65,) // 마지막 쉼표를 생략할 수 있습니다
var _ = string(65,  // 마지막 쉼표가 필요합니다
)
하지만 다음의 코드는 유효하지 않습니다. 컴파일러가 두 번째 줄을 제외한 나머지 줄에는 모두 끝에 세미콜론을 삽입할 것이기 때문이죠. 총 세 개의 줄에서 unexpected newline 구문 오류가 발생할 겁니다.
func f1(a int, b string,) (x bool, y int // error
) {
	return true, 789
}
var _ = []int{2, 3, 5, 7 // error: unexpected newline
}
var _, _ = f1(123, "Go" // error: unexpected newline
)

장을 마치며

마치며, 위의 설명을 생각하며 Go에서의 줄 바꿈 규칙을 정리해봅시다.

Go에서 줄 바꿈을 해도 되는 (하더라도 코드 동작에 영향을 주지 않는) 조건은 다음과 같습니다.

다른 여러 Go의 디자인과 마찬가지로, 세미콜론 삽입 규칙은 찬사와 비판을 동시에 받고 있습니다. 어떤 프로그래머들은 이 규칙이 코드 스타일의 자유를 제한한다고 생각해 마음에 들어 하지 않기도 합니다. 반면 좋아하는 사람들은 이 규칙 덕분에 컴파일이 더 빨라질 뿐 아니라, 여러 프로그래머가 작성한 코드가 보기에 비슷해져 서로의 코드를 더 쉽게 이해할 수 있게 해준다고 생각합니다.


Index↡

The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: