여러분이 C/C++ 혹은 자바에 경험이 있다면 Go언어는 최근에 나온 다른 개발언어들보다 빨리 익힐 수 있을 것이다.
Go언어는 C언어 계열이다
Go언어는 공식적으로 C언어 계열이다. C언어를 만들었던 켄 톰슨이 Go언어의 핵심 개발자이며 현재 가장 널리 사용되는 언어가 C언어이므로 C언어 계열이라는 것은 큰 장점이다. C언어를 버리고 새로운 형태의 언어를 만드는 것은 어렵기도 하고 대중에게 빠른 시간내에 퍼지기도 어려울 것이다. 따라서 C/C++ 혹은 자바에 경험이 있는 개발자라면 Go언어를 익히는 데 많은 노력이 들지는 않을 것이라 생각한다. C언어에서 Go언어로 오면서 변형된 문법이나 추가된 기능만 익힌다면 쉽게 사용할 수 있다.
이번 호에서는 이 두 언어의 두드러진 차이점을 한 눈에 살펴본 후에 Go언어의 특징을 직관적인 것, 제거된 것, 변경된 것, 추가된 것, 엄격한 것으로 나눠서 알아보자. 마지막으로는 Go언어 testing 패키지를 이용한 단위 테스트도 소개하겠다.
Go언어는 공식적으로 C언어 계열이다. C언어를 만들었던 켄 톰슨이 Go언어의 핵심 개발자이며 현재 가장 널리 사용되는 언어가 C언어이므로 C언어 계열이라는 것은 큰 장점이다. C언어를 버리고 새로운 형태의 언어를 만드는 것은 어렵기도 하고 대중에게 빠른 시간내에 퍼지기도 어려울 것이다. 따라서 C/C++ 혹은 자바에 경험이 있는 개발자라면 Go언어를 익히는 데 많은 노력이 들지는 않을 것이라 생각한다. C언어에서 Go언어로 오면서 변형된 문법이나 추가된 기능만 익힌다면 쉽게 사용할 수 있다.
이번 호에서는 이 두 언어의 두드러진 차이점을 한 눈에 살펴본 후에 Go언어의 특징을 직관적인 것, 제거된 것, 변경된 것, 추가된 것, 엄격한 것으로 나눠서 알아보자. 마지막으로는 Go언어 testing 패키지를 이용한 단위 테스트도 소개하겠다.
한 눈에 비교하기
Go언어와 C++ 그리고 자바에서 지원하는 기능을 열거하고 각 언어에서 해당 기능을 지원하는지의 여부를 표로 만들었다(<표 1> 참조). 물론 세부적으로 들어가면 비교할 것이 많겠지만 Go언에서 특징이 부각되도록 기능들을 열거했다. 3분 정도 표만 보고 Go언어의 특징을 음미해 보자.
Go언어와 C++ 그리고 자바에서 지원하는 기능을 열거하고 각 언어에서 해당 기능을 지원하는지의 여부를 표로 만들었다(<표 1> 참조). 물론 세부적으로 들어가면 비교할 것이 많겠지만 Go언에서 특징이 부각되도록 기능들을 열거했다. 3분 정도 표만 보고 Go언어의 특징을 음미해 보자.
<표 1> 각 개발언어와 기능지원 여부
이 표만 보고 Go언어에 대해 드는 느낌은 무엇인가? 아마 여러 가지 의문이 들었을 것이라 예상된다. 자, 그렇다면 함께 <표 1>에서 나타난 Go의 특징을 하나씩 살펴보자. 우선 Go는 클래스(Class)를 지원하지 않는다. 따라서 상속(inheritance)이나 오버로딩(overloading)도 지원하지 않는다. 그리고 가비지 콜렉터(garbage collector)를 지원한다. 기존 시스템 프로그래밍 언어인 C나 C++에서는 개발자가 할당받은 메모리에 대해 직접 해제해야 하는데 Go언어에서는 자바와 같이 개발자가 메모리의 해제에 대해 신경 쓸 필요가 없다.
다음으로 Go언어의 특징은 포인터(Pointer)는 있는데 포인터 연산이 없다는 것이다. 반면에 C나 C++에서는 포인터와 포인터 연산이 모두 가능하다. 다른 모듈이나 패키지를 사용할 때는 자바와 비슷한 방법으로 구현하며, 자바의 인터페이스(interface)와 유사하게 동일한 키워드로 인터페이스를 지원한다. 다른 두 언어에서 널(null) 값으로 사용하는 것을 Go언어에서는 ‘nil’로 표현한다는 차이가 있다.
그리고 마지막으로 Go언어는 타입변환(type conversion)에 대해 아주 엄격하다. 다른 두 언어에서는 경고(warning) 정도로 넘어가는 것이 Go언어에서는 컴파일 에러(compile error)를 발생시킨다. 이렇게 <표 1>을 통해 Go언어의 특징을 알아봤다. 이제 Go언어의 특징에 대해 감이 잡히는가? 설명이 부족했다면 하나씩 좀 더 깊이 알아보자.
다음으로 Go언어의 특징은 포인터(Pointer)는 있는데 포인터 연산이 없다는 것이다. 반면에 C나 C++에서는 포인터와 포인터 연산이 모두 가능하다. 다른 모듈이나 패키지를 사용할 때는 자바와 비슷한 방법으로 구현하며, 자바의 인터페이스(interface)와 유사하게 동일한 키워드로 인터페이스를 지원한다. 다른 두 언어에서 널(null) 값으로 사용하는 것을 Go언어에서는 ‘nil’로 표현한다는 차이가 있다.
그리고 마지막으로 Go언어는 타입변환(type conversion)에 대해 아주 엄격하다. 다른 두 언어에서는 경고(warning) 정도로 넘어가는 것이 Go언어에서는 컴파일 에러(compile error)를 발생시킨다. 이렇게 <표 1>을 통해 Go언어의 특징을 알아봤다. 이제 Go언어의 특징에 대해 감이 잡히는가? 설명이 부족했다면 하나씩 좀 더 깊이 알아보자.
Go는 객체지향언어?
클래스가 없다. 상속도 없다. 당연히 오버로딩도 지원하지 않는다. 그렇다면 Go언어는 객체지향언어일까? “그렇기도 하고 아니기도 하다”고 Go언어를 만든 사람들은 말한다. 타입(type)과 메소드(method)를 갖고 객체지향 스타일의 프로그래밍이 가능하지만 상속은 지원하지 않고 인터페이스(interface)라는 개념으로 다른 접근방식을 지원한다. 임베드 타입(embed type)을 이용하면 서브클래스(subclass)와 유사한 기능을 제공할 수 있다. 따라서 완벽하게 기존의 객체지향을 지원하지는 않지만 비슷한 효과를 낼 수는 있다.
클래스가 없다. 상속도 없다. 당연히 오버로딩도 지원하지 않는다. 그렇다면 Go언어는 객체지향언어일까? “그렇기도 하고 아니기도 하다”고 Go언어를 만든 사람들은 말한다. 타입(type)과 메소드(method)를 갖고 객체지향 스타일의 프로그래밍이 가능하지만 상속은 지원하지 않고 인터페이스(interface)라는 개념으로 다른 접근방식을 지원한다. 임베드 타입(embed type)을 이용하면 서브클래스(subclass)와 유사한 기능을 제공할 수 있다. 따라서 완벽하게 기존의 객체지향을 지원하지는 않지만 비슷한 효과를 낼 수는 있다.
가비지 콜렉션
시스템 프로그래밍에서 가장 많은 코딩 부분을 차지하는 것 중에 하나가 메모리 관리(memory management)다. 버그가 가장 많이 발생하는 부분이기도 하고 프로그래머가 가장 많은 시간을 들이면서 작업하는 부분이기도 하다. 따라서 개발자의 노력을 줄이기 위해서도 가비지 콜렉션이 필요하다. 이미 자바에서는 가상 머신을 지원하고 있지만 Go언어처럼 독립적으로 실행 가능한 파일을 생성하는 경우 가비지 콜렉션을 지원하는 것은 쉽지 않은 일이다. 시스템 프로그래밍 코드가 간결하고 안정적으로 동시성(Concurrency)를 지원하는 코드를 쉽게 작성하기 위해 Go언어에서는 가비지 콜렉션을 지원한다.
시스템 프로그래밍에서 가장 많은 코딩 부분을 차지하는 것 중에 하나가 메모리 관리(memory management)다. 버그가 가장 많이 발생하는 부분이기도 하고 프로그래머가 가장 많은 시간을 들이면서 작업하는 부분이기도 하다. 따라서 개발자의 노력을 줄이기 위해서도 가비지 콜렉션이 필요하다. 이미 자바에서는 가상 머신을 지원하고 있지만 Go언어처럼 독립적으로 실행 가능한 파일을 생성하는 경우 가비지 콜렉션을 지원하는 것은 쉽지 않은 일이다. 시스템 프로그래밍 코드가 간결하고 안정적으로 동시성(Concurrency)를 지원하는 코드를 쉽게 작성하기 위해 Go언어에서는 가비지 콜렉션을 지원한다.
포인터 연산이 없다?
포인터는 있는데 왜 포인터 연산을 지원하지 않을까? C개발자라면 느끼겠지만 심각한 버그의 상당부분은 포인터 연산에서 기인한다. 따라서 안전성을 위해 포인터 연산을 지원하지 않는다. 잘못된 주소에 접근해 값을 갖고 오기도 하고 잘못된 주소에 값을 쓰기도 한다. 이처럼 잘못된 주소로 갈 수 있는 포인트 연산을 Go언어에서는 지원하지 않으므로 안전한 프로그래밍이 가능하다.
포인터는 있는데 왜 포인터 연산을 지원하지 않을까? C개발자라면 느끼겠지만 심각한 버그의 상당부분은 포인터 연산에서 기인한다. 따라서 안전성을 위해 포인터 연산을 지원하지 않는다. 잘못된 주소에 접근해 값을 갖고 오기도 하고 잘못된 주소에 값을 쓰기도 한다. 이처럼 잘못된 주소로 갈 수 있는 포인트 연산을 Go언어에서는 지원하지 않으므로 안전한 프로그래밍이 가능하다.
직관적인 것
Go언어를 처음 접한 사람에게는 생소하지만 직관적인 변수 선언 부분을 살펴보자.
C나 자바 개발자가 Go언어를 처음 접할 때 가장 거부감을 느끼는 부분이 변수 선언부다. Go언어에서는 변수 선업부가 더 길고 기존에 알던 순서와 반대다. 필자의 주변 개발자들도 이런 불평을 하는 경우를 봤다. “뭐야 C언어 계열이라고 하면서 가장 기본적인 변수선언도 다르고 예전보다 더 복잡해진 것 같잖아”라고. 필자도 처음에는 왜 이렇게 바뀌었는지 이해하기가 어려웠다. 초기에 자료도 부족했고 마땅히 설명할 수 있는 논리가 없어서 답답했던 기억이 난다. 이후 Go언어 관련 발표에서 Go언어 개발자가 직접 이렇게 변수를 선언하는 이유를 설명했다. 비로소 “아하! 그래서 이렇게 생겼구나” 이해할 수 있었다. 지금부터 그 이유를 알아보자.
Go언어를 처음 접한 사람에게는 생소하지만 직관적인 변수 선언 부분을 살펴보자.
C나 자바 개발자가 Go언어를 처음 접할 때 가장 거부감을 느끼는 부분이 변수 선언부다. Go언어에서는 변수 선업부가 더 길고 기존에 알던 순서와 반대다. 필자의 주변 개발자들도 이런 불평을 하는 경우를 봤다. “뭐야 C언어 계열이라고 하면서 가장 기본적인 변수선언도 다르고 예전보다 더 복잡해진 것 같잖아”라고. 필자도 처음에는 왜 이렇게 바뀌었는지 이해하기가 어려웠다. 초기에 자료도 부족했고 마땅히 설명할 수 있는 논리가 없어서 답답했던 기억이 난다. 이후 Go언어 관련 발표에서 Go언어 개발자가 직접 이렇게 변수를 선언하는 이유를 설명했다. 비로소 “아하! 그래서 이렇게 생겼구나” 이해할 수 있었다. 지금부터 그 이유를 알아보자.
<리스트 1> C에서 변수 선언
int a; int b[10]; |
<리스트 2> Go에서 변수 선언
var a int var b [10]int |
C나 자바에서의 변수 선언은 <리스트 1>과 같다. 우리에게 이미 익숙한 표현이다. <리스트 2>의 Go언어에서의 변수선언을 보자. 앞에 var가 하나 더 붙어 있고 C언어에서의 선언 순서와 반대다. 왜 이렇게 복잡하게 표현했을까? Go언어를 만든 개발자가 밝히는 변수 선언 문법의 비밀은 마치 영어 문장으로 쓰는 것처럼 표현하고자 했다는 것이다. 즉, “변수 a는 정수형이다.”를 영어 문장으로 표현한다면 “Variable a is integer”가 된다.
기존 방식보다 직관적으로 느껴지는가? 우리는 영어를 모국어로 하지 않기에 직관적인 느낌을 얻기보다는 이전에 친근했던 것과 다른 것에 거부감이 들지도 모르겠다. 하지만 <리스트 3>과 <리스트 4>와 같은 경우를 보면 좀더 Go언어의 표현이 직관적이라고 생각할 수 있을 것이다. C언어로 변수를 선언한 <리스트 3>을 보자. c와 d는 같은 타입일까? 코드를 읽거나 작성할 때 오류를 내기 쉬운 경우다. <리스트 4>의 Go언어에서는 b, c가 동일하게 정수형 포인터로 선언된다.
<리스트 3> C에서 변수 선언
int* c, d; |
<리스트 4> Go에서 변수 선언
var c, d *int |
변수 선언시 <리스트 5>와 같이 다양한 방법으로 코드 타이핑 양을 줄일 수도 있다.
<리스트 5> Go에서 다양한 변수 선언 방법
// var를 사용해 그룹으로 묶기 var( a int f float64 s string )
// 선언시 값을 할당하는 경우 type 생략 가능
var i = 4
// 함수내에서만 사용되며 ‘:=’로서 var, type 생략 가능
i := 1 |
Go언어를 시작할 때 혼란을 겪는 첫 번째 장애물이 바로 변수 선언이다. 기존 방법에서 변경된 이유와 사용법을 알면 Go언어로 된 소스를 읽기가 한결 쉬울 것이다. Go언어에 적용된 문법들은 직관적이며 코드 타이핑 양을 줄이는 방향으로 변화했다는 것을 기억하고 이 글을 읽는다면 도움이 될 것이다.
제거된 것
이제 Go언어에서 제거된 것들을 살펴보자.
이제 Go언어에서 제거된 것들을 살펴보자.
세미콜론
<리스트 6> 세미콜론이 생략된 선언
var i int |
C뿐만 아니라 자바에서도 스테이트먼트(statement)가 끝날 때 세미콜론(‘;’)을 넣는다. <리스트 6>와 같이 Go언어에서는 세미콜론을 사용하지 않고 변수를 선언했다. 실제로 Go언어에서는 세미콜론을 사용할 일이 거의 없다. Go언어가 제공하는 패키지 소스를 봐도 for나 if문에서의 사용을 제외하고는 거의 찾아보기가 힘들다.
기존 언어에서 세미콜론은 해당 스테이트먼트가 종료된다는 것을 의미한다. Go언어에서는 세미콜론을 제거했다기보다는 생략할 수 있다는 것이 정확한 표현이다. 따라서 세미콜론을 넣어줘도 아무런 문제가 되지 않는다. 컴파일시 컴파일러가 자동으로 ‘;’를 삽입하기 때문이다. Go언어에서도 여러 스테이트먼트를 하나의 라인에 표현할 때에 ‘;’를 사용할 수 있다.
<리스트 7> Go에서 if문 사용시 주의할 사항
if i == 0 { //올바른 사용 f() } if i == 0 // 컴파일 에러. ‘;’이 삽입돼 if i == 0 ;이 되기 때문이다. { f() } // 아래와 같은 오류 발생 prog.go:19: missing condition in if statement prog.go:19: i == 0 not used |
컴파일러가 자동으로 세미콜론을 삽입하므로 <리스트 7>에서 보는 바와 같이 주의해서 사용해야 한다. if문이나 for문과 같이 ‘{...}’를 사용하며 ‘{’를 같은 라인에 붙여서 사용한다. C 개발자가 자주 범하기 쉬운 오류다. 또 C언어에서는 if나 for를 사용하는 경우 ‘{}’를 생략할 수 있는 경우도 있지만 Go언어에서는 반드시 ‘{}’를 사용해야만 한다.
C언어에서는 ‘if (condition)’에서 ‘()’를 사용하지만 Go에서는 <리스트 7>에서와 같이 생략이 가능하다.
while
<리스트 8> C에서 whlie문
while(condition) { //....} |
<리스트 9> Go에서 for로 while 구현 방법
for condition{ //....} |
<리스트 8>과 같이 C나 자바에서는 while 키워드를 사용해 반복제어문을 구현할 수 있다. for와 유사한 동작을 한다. C언어를 처음 배울 때 ‘while과 for의 차이점은 대체 무엇일까?’라는 의문을 한 번쯤 가져봤을 것이다. 어떤 경우에 for를 쓰고, while을 쓰는 경우는 또 어떤 경우일까? 실제로 구글에서 ‘for while 차이점’으로 검색해 보면 많은 답변을 찾을 수 있다. 동일한 기능을 구현할 수 있으며, 코드 가독성이나 컴파일러마다 성능 차이를 낼 수 있다는 정도로 판단할 수 있다. Go언어에는 ‘while’이라는 키워드가 없다. <리스트 9>와 같이 for를 사용해서 while을 구현한다. for 이후에 조건이 오면 while과 동일하다. 사용방식만 다르고 동일한 기능을 수행하던 것을 하나로 묶었으며 마치 중복된 코드에 리팩터링을 해서 중복을 제거한 느낌이 든다.
접근 제한자 - public, private
<리스트 10> 자바에서 접근 제한자
Class A{ public void f(){ } private void g(){ } } |
<리스트 11> Go에서 접근 제한 방법
func F(){
}
func g(){
}
|
지금까지 본 바로도 Go언어에서는 불필요한 키워드를 최대한 줄이려고 노력했다는 것을 알 수 있다. 그 중에 가장 재밌다고 생각되는 표현 중에 하나는 자바에서 사용하는 public, private과 유사한 기능에 대한 문법이다. 모듈 내부에서 사용하는 경우에는 소문자로, 외부에서 접근할 수 있도록 하기 위해서는 대문자로 시작하면 된다.
자바에서 클래스 외부에서 접근을 허용하는 경우 <리스트 10>과 같이 ‘public’이라는 키워드를 사용한다. 그리고 클래스 내부로 제한하는 경우에는 ‘private’이라는 키워드를 사용한다. <리스트 11>에서와 같이 Go언어에서는 함수의 첫 글자를 ‘F’와 같이 대문자로 시작하는 경우 외부 모듈에서 접근 가능한 public이 되고 ‘g’와 같이 소문자로 시작하는 경우 외부 모듈에서 접근할 수 없게 된다. 이렇게 함으로써 키워드 두 개(public, private)를 절약하는 효과가 있다.
초기화
<리스트 12> Go에서의 사용하는 zero values
numeric : 0 boolean : false string : "" pointer, map, slice, channel : nil struct : zeroed struct |
C나 자바로 개발하는 경우 함수나 메소드 내에서 변수를 선언하고 개발자가 의도한 값으로 초기화를 수행한다. 실제 코딩을 하다보면 대개의 경우 묵시적으로 특정값을 사용해서 초기화시킨다. Go언어에서는 변수를 선언하면 자동으로 미리 정의한 <리스트 12>의 ‘zero value’로 초기화한다. 따라서 개발자가 초기화 코드를 따로 넣을 필요가 없다. Go언어에서 정의한 ‘zero value’는 C나 자바로 개발할 때 묵시적으로 자주 사용하던 값과 일치하는 것을 알 수 있다. 따라서 기억하는 데 크게 노력이 들지 않는다. <리스트 12>는 각 타입에 대한 zero value다.
변경된 것
이번에는 변경된 사항들을 짚어보자.
이번에는 변경된 사항들을 짚어보자.
인터페이스
자바에서 제공하는 인터페이스와 유사하다. 하지만 사용하는 방법이 다르다. Go언어를 개발한 롭 파이크(Rob Pike)는 자바의 인터페이스보다 ‘참신(novel)’하다고 표현한 바 있다.
인터페이스의 정의는 ‘메소드들의 집합’이다. 이런 정의들만으로는 감이 오지 않을 것이다. 필자가 Go언어의 인터페이스를 이해하기 위해 했던 방법은 자바의 인터페이스로 구현한 것을 동일하게 Go언어로 구현해 보는 것이다. <리스트 13>부터 <리스트 20>을 통해 인터페이스에 대해 감을 잡을 수 있을 것이다.
자바에서 제공하는 인터페이스와 유사하다. 하지만 사용하는 방법이 다르다. Go언어를 개발한 롭 파이크(Rob Pike)는 자바의 인터페이스보다 ‘참신(novel)’하다고 표현한 바 있다.
인터페이스의 정의는 ‘메소드들의 집합’이다. 이런 정의들만으로는 감이 오지 않을 것이다. 필자가 Go언어의 인터페이스를 이해하기 위해 했던 방법은 자바의 인터페이스로 구현한 것을 동일하게 Go언어로 구현해 보는 것이다. <리스트 13>부터 <리스트 20>을 통해 인터페이스에 대해 감을 잡을 수 있을 것이다.
<리스트 13> 자바에서 Drawer 인터페이스 정의
interface Drawer{ public void Draw(); } |
<리스트 14> Go언어에서 Drawer 인터페이스 정의
type Drawer interface{ Draw() } |
<리스트 15> 자바에서 Circle 클래스 구현
class Circle implements Drawer{ int r; Circle(int r){ this.r = r; } public void Draw(){ System.out.println("Circle is Draw : "+ r); } } |
<리스트 16> Go언어에서 Circle 구조체 및 메소드 구현
type Circle struct{ r int } func (c Circle) Draw(){ fmt.Println("Circle is Draw : ", c.r) } |
<리스트 17> 자바에서 Rectangle 클래스 구현
class Rectangle implements Drawer{ int w, h; Rectangle(int w, int h){ this.w = w; this.h= h; } public void Draw(){ System.out.println("Rectangle is w="+w+" h="+ h); } } |
<리스트 18> Go언어에서 Rectangle 구조체 및 메소드 구현
type Rectangle struct{ w, h int } func (r Rectangle) Draw(){ fmt.Println("Rectangle is w=", r.w, " h=", r.h) } |
<리스트 19> 자바에서 인터페이스 사용
public class MyMain { public static void DrawForm(Drawer d){ d.Draw(); } public static void main(String[] args) { Circle myC = new Circle(5); DrawForm(myC); Rectangle myR = new Rectangle(3, 4); DrawForm(myR); } } |
<리스트 20> Go언어에서 인터페이스 사용
func DrawForm(d Drawer){ d.Draw() }
func main() {
var myC Circle = Circle{5} var myR Rectangle = Rectangle{3, 4} DrawForm(myC) DrawForm(myR) } |
switch
C에서 switch문을 사용할 때 가장 실수하기 쉬운 부분이 break문이다. break를 넣어야 하는데 넣지 않은 경우와 넣지 말아야 하는데 넣은 경우가 문제가 된다. switch 내부에 break문을 쓰거나 생략하는 등 복잡하게 사용해 그 의도가 명확하게 드러나지 않는 경우도 많다. 필자가 경험한 한 개발부서에서는 break를 무조건 넣는 것을 코딩룰(coding rule)로 정하고 있다. Go언어에서는 직관적이며 간결화시키는 방법으로 이 문제에 접근한다. <리스트 21>처럼 switch문에서 break문을 사용하지 않는다. 또 의도를 명확히 하기 위해 <리스트 22>처럼 다른 case지만 동일한 동작을 해야 하는 경우 각 case를 ‘,’로 연결해 표현한다. switch문에 사용하는 조건은 상수뿐만 아니라 비교연산이 가능한 것이라면 어떤 것이라도 가능하다. 따라서 string도 사용가능하다.
switch만 있는 경우에는 기본적으로 true가 된다. 기존에 if/else if/else로 표현했던 것을 다음의 <리스트 23>과 같이 표현할 수 있다.
C에서 switch문을 사용할 때 가장 실수하기 쉬운 부분이 break문이다. break를 넣어야 하는데 넣지 않은 경우와 넣지 말아야 하는데 넣은 경우가 문제가 된다. switch 내부에 break문을 쓰거나 생략하는 등 복잡하게 사용해 그 의도가 명확하게 드러나지 않는 경우도 많다. 필자가 경험한 한 개발부서에서는 break를 무조건 넣는 것을 코딩룰(coding rule)로 정하고 있다. Go언어에서는 직관적이며 간결화시키는 방법으로 이 문제에 접근한다. <리스트 21>처럼 switch문에서 break문을 사용하지 않는다. 또 의도를 명확히 하기 위해 <리스트 22>처럼 다른 case지만 동일한 동작을 해야 하는 경우 각 case를 ‘,’로 연결해 표현한다. switch문에 사용하는 조건은 상수뿐만 아니라 비교연산이 가능한 것이라면 어떤 것이라도 가능하다. 따라서 string도 사용가능하다.
switch만 있는 경우에는 기본적으로 true가 된다. 기존에 if/else if/else로 표현했던 것을 다음의 <리스트 23>과 같이 표현할 수 있다.
<리스트 21> Go언어에서 일반적인 switch 사용
switch i { case 0: // i가 0인 경우 하는 일이 없으며 break를 사용하지 않는다. case 1: // i가 1인 경우만 수행한다. doSomething() } |
<리스트 22> Go언어에서 같은 동작을 수행하는 case들
switch i { case 0, 1: // i가 0 혹은 1인 경우 수행한다. 의도가 명확히 표현됨. doSomething() } |
<리스트 23> Go언어에서 switch 이후 생략된 경우
switch { // switch만 있는 경우 true를 기본값으로 가진다. case i < 0: f1() case i == 0: f2() case i > 0: f3() } |
함수
<리스트 24>는 C에서, <리스트 25>는 Go언어에서 함수를 사용하는 경우다. 아주 비슷한 형태다. 가장 두드러진 차이점은 Go언어에서 함수를 사용하는 경우 앞에 ‘func’ 키워드를 붙여준다는 것이다. 그리고 반환 타입에 대한 정의가 C에서는 함수 시작부분에 왔지만 Go언어에서는 맨 마지막에 왔다. 실제 이 함수의 동작을 영작해 보면 ‘Function add returns integer’로 표현할 수 있으며 반환 타입이 함수의 끝에 오는 것이 더 직관적일 수 있다.
<리스트 24>는 C에서, <리스트 25>는 Go언어에서 함수를 사용하는 경우다. 아주 비슷한 형태다. 가장 두드러진 차이점은 Go언어에서 함수를 사용하는 경우 앞에 ‘func’ 키워드를 붙여준다는 것이다. 그리고 반환 타입에 대한 정의가 C에서는 함수 시작부분에 왔지만 Go언어에서는 맨 마지막에 왔다. 실제 이 함수의 동작을 영작해 보면 ‘Function add returns integer’로 표현할 수 있으며 반환 타입이 함수의 끝에 오는 것이 더 직관적일 수 있다.
<리스트 24> C에서 add 함수
int add(int a, int b){ return a+b } |
<리스트 25> Go언어에서 add 함수
func add(a,b int) int{ return a+b } |
<리스트 26>은 동일한 타입을 갖는 정수형인 i, j, k와 문자열 타입인 s, t로 묶을 수 있다. 이를 통해 코드가 한결 짧아지고 깔끔해졌다.
<리스트 26> Go언어에서 동일 함수 인자 묶기
func f(i int, j int, k int, s string, k string) func f(i, j, k int, s, t string) |
<리스트 27> Go언어에서 두 개 이상의 값을 반환
func add(a, b int) (int, bool){ return a+b, true } |
두 개 이상의 값을 반환할 수 있다. <리스트 27>에서는 정수형, 불린(Boolean)형 이렇게 두 개의 값을 리턴하는 경우를 예로 보여준다.
배열
<리스트 28>은 C의 배열보다는 파스칼(Pascal)에서 사용하는 배열과 유사하다. 여기서는 세 개의 정수형을 갖는 배열 ar을 선언했다. Go언어는 선언시 ‘zero value’로 초기화된다. C에서 사용하는 배열과 가장 구별되는 특징은 포인터가 아니라 값(value)이라는 것이다. 사이즈를 구하기 위해 len()을 사용한다. <리스트 28>의 경우 실행하면 3이 된다.
<리스트 28>은 C의 배열보다는 파스칼(Pascal)에서 사용하는 배열과 유사하다. 여기서는 세 개의 정수형을 갖는 배열 ar을 선언했다. Go언어는 선언시 ‘zero value’로 초기화된다. C에서 사용하는 배열과 가장 구별되는 특징은 포인터가 아니라 값(value)이라는 것이다. 사이즈를 구하기 위해 len()을 사용한다. <리스트 28>의 경우 실행하면 3이 된다.
<리스트 28> Go언어에서 배열선언과 사이즈 구하기
var ar [3]int len(ar) |
<리스트 29> Go언어에서 배열 리터럴
[3]int{1, 2, 3} // 3개 정수를 갖는 배열 [10]int{1, 2, 3} // 10개 정수를 갖는 배열 중 처음 3개에 값을 대입 [...]int{1, 2, 3} // [...]을 가지는 경우 {}안에 있는 엘리먼트의 갯수로 결정된다. 따라서 [3]int 배열이 된다. |
추가된 것
Go언어에는 스왑, 슬라이스, Defer가 추가됐다.
Go언어에는 스왑, 슬라이스, Defer가 추가됐다.
스왑
<리스트 30> C에서 스왑
int i, j, temp;
temp = i; i = j; j = temp; |
<리스트 31> Go에서의 스왑
i, j = j, i
|
스왑은 두 변수에 들어있는 값을 서로 맞바꾸는 연산이다. 많은 정렬 알고리즘에서 값들의 순서를 바꾸기 위해 사용된다. <리스트 30>은 C나 자바에서의 구현방법이다. 반면에 Go언어에서는 스왑을 <리스트 31>과 같이 표현할 수 있다. 예제에서는 두 개 변수의 스왑을 보여줬지만 물론 두 개 이상의 값들도 동일한 방법으로 스왑이 가능하다.
슬라이스
포인터 연산이 없는 대신 배열(array)의 특정 부분을 참조(reference)하기 위해 슬라이스가 사용된다. 개념적으로 슬라이스는 0번째 엘리먼트(element)를 가리키는 포인터와 슬라이스에 있는 엘리먼트의 개수(length), 담을 수 있는 최대 엘리먼트 허용 개수(capacity) 이렇게 세 부분으로 구성된다. <리스트 32>에서 배열과 슬라이스의 선언방법의 차이를 알 수 있다. 배열에서 사이즈를 지정하는 부분이 없으면 슬라이스가 된다. 실제로 Go언어의 소스를 보다보면 배열보다 슬라이스가 더 많이 보인다.
포인터 연산이 없는 대신 배열(array)의 특정 부분을 참조(reference)하기 위해 슬라이스가 사용된다. 개념적으로 슬라이스는 0번째 엘리먼트(element)를 가리키는 포인터와 슬라이스에 있는 엘리먼트의 개수(length), 담을 수 있는 최대 엘리먼트 허용 개수(capacity) 이렇게 세 부분으로 구성된다. <리스트 32>에서 배열과 슬라이스의 선언방법의 차이를 알 수 있다. 배열에서 사이즈를 지정하는 부분이 없으면 슬라이스가 된다. 실제로 Go언어의 소스를 보다보면 배열보다 슬라이스가 더 많이 보인다.
<리스트 32> C에서의 배열과 슬라이스 선언
var ar [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // 배열 ar var a []int // 슬라이스 a |
<리스트 33> Go에서의 다양한 슬라이스 이용방법
a = ar[7:9] / / ar의 7, 8번째 값을 참조한다. len(a) // a의 사이즈는 2가 된다. a = ar[:n] // ar[0:n] array의 0번째부터 n-1번째까지 참조한다. a = ar[n:] // ar[n:len(a)] array의 n번째부터 끝까지를 참조한다. a = ar[:] // ar[0:len(ar)] 즉 array전체를 참조한다. |
슬라이스의 경우 메모리를 할당하는 방식이 아니라 참조하는 방식이다. 따라서 비용이 적게 들어 부담 없이 필요할 때마다 사용하면 된다. <리스트 33>은 배열과 슬라이스를 이용하는 방법에 대해 설명한다.
Defer
<리스트 34> C에서의 File IO
int main() { FILE *fs; char ch;
fs = fopen("test.txt", "r");
if (fs == NULL) exit(); while( 1 ) { ch = fgetc(fs);
if (ch == EOF)
break; else doSomething(ch); } fclose ( fs ) ; } |
<리스트 35> Go에서의 File IO
func data(fileName string) string { f := os.Open(fileName) defer f.Close() contents := io.ReadAll(f) return contents } |
Go언어에 추가된 키워드 중에 ‘defer’가 있다. 가장 유용할 때가 파일 입출력(File IO) 기능을 구현할 때다. <리스트 34>는 C로 구현된 코드로 파일을 열어서 파일의 끝까지 읽으면서 처리한다. 파일을 다 읽은 후에는 마지막으로 파일을 닫는 fclose(fs)를 호출한다. <리스트 35>은 Go언어로 구현한 비슷한 예제다. 파일을 열기 위해 os.Open()을 하고 바로 f.Close()가 호출된다. f.Close()는 분명히 파일을 읽기 위한 동작인 io.ReadAll()를 마친 뒤에 호출되어야 마땅하다. 이쯤 되면 f.Close()와 함께 사용된 defer가 하는 일을 짐작할 수 있을 것이다.
<리스트 35>에서 data() 함수가 반환되는 시점에 defer 키워드를 사용한 함수가 호출된다. 정리하자면 Open → ReadAll → Close 순으로 호출된다. Open, Close와 같이 열고 닫는 동작이 서로 쌍으로 존재하는 경우 두 함수를 함께 붙여서 사용할 수 있다. 이렇게 함으로써 가독성이 높아지며 잊지 않게 도와주는 효과가 있다. 파일을 닫거나 뮤텍스(mutexes)의 락(lock)을 해제하는 경우에도 유용하게 사용된다.
엄격한 것
마지막으로 Go언어에서 엄격해진 것을 알아보자.
마지막으로 Go언어에서 엄격해진 것을 알아보자.
선언 후 사용하지 않는 변수
<리스트 36> Go에서 선언 후 사용하지 않는 변수
func add(i, j int) int{ result := 0 return i + j } |
<리스트 37> Go에서의 컴파일 에러 발생
prog.go:5: result declared and not used |
처음 Go언어로 프로그래밍을 할 때 가장 짜증나게 혹은 괴롭히는 것 중에 하나를 소개하려고 한다. <리스트 36>에서 result 변수를 선언하고 사용하지 않는 경우 <리스트 37>과 같이 컴파일 에러가 발생한다.
생각나는 변수들을 먼저 선언하고 소스를 작성하는 경우가 많다. 막상 구현을 한 후 해당 변수를 사용하지 않는 경우가 자주 있다. 이럴 때마다 <리스트 37>과 같은 컴파일 오류가 발생한다. 컴파일 단계에서 코드에 불필요한 변수는 반드시 걸러내겠다는 확고한 신념이 느껴진다. 불필요한 변수 선언을 하지 않게 되어 깔끔한 코드를 유지하는데 확실히 효과가 있다. 이런 제약을 잘 이용하면 좋은 프로그래밍 습관을 기를 수도 있다.
암시적 형변환(implicit type conversion)
<리스트 38> 암시적 형변환
var i16 int16 var i32 int32 i32 = i16 |
<리스트 39> 명시적 형변환
var i16 int16 var i32 int32 i32 = int32(i16) |
암시적 형변환은 컴파일러가 자동으로 형변환을 하는 것을 말한다. Go언어는 버그가 발생할 여지를 최소화하는 데 주력하고 있다. 기존 개발언어에서는 컴파일시 경고로 충분했을지 모르지만 Go언어에서는 컴파일시 에러가 발생한다. <리스트 38>과 같이 Go언어에서 암시적 형변환을 시도하는 경우 ‘prog.go:15: cannot use i16 (type int16) as type int32 in assignment’과 같은 컴파일 에러를 보게 된다. <리스트 39>처럼 Go언어에서는 명시적인 형변환을 해야 한다. int16을 int32에 대입할 때도 반드시 명시적인 형변환을 해야만 한다.
<리스트 40> Go에서 타입명이 다르면 명시적인 형변환
type MyInt int var i int var j MyInt
func main(){
i = 3 //j = i // 결과는 : cannot use i (type int) as type MyInt in assignment
j = MyInt(i) // 반드시 명시적인 형변환이 필요하다.
} |
<리스트 40>은 좀 더 극단적인 경우다. MyInt라는 새로운 타입을 정의했다. int와 MyInt가 같다고 생각해 j = i처럼 대입하면 컴파일 에러가 발생한다. 타입명이 일치하지 않으면 반드시 명시적인 형변환이 필요하다.
단위 테스트
단위 테스트(unittest)는 안심하고 개발언어와 라이브러리를 사용할 수 있는 안전망 역할을 한다. 그렇다면 Go언어에서는 어떨까? Go언어는 설치할 때 단위 테스트를 동시에 실행한다. 자신의 개발환경에서 실제 라이브러리들의 실행 결과를 바로 확인할 수 있으므로 안정감을 준다.
단위 테스트(unittest)는 안심하고 개발언어와 라이브러리를 사용할 수 있는 안전망 역할을 한다. 그렇다면 Go언어에서는 어떨까? Go언어는 설치할 때 단위 테스트를 동시에 실행한다. 자신의 개발환경에서 실제 라이브러리들의 실행 결과를 바로 확인할 수 있으므로 안정감을 준다.
<리스트 41> 패턴찾기 문제
aaaaa -> a ababab -> ab abaaba -> aba c.c.c. -> c. |
<리스트 42> 테스트 작성 - pattern_test.go
//pattern_test.go package pattern
import (
"testing" ) type patternTest struct{ in, out string }
var patternTests = []patternTest{
patternTest{"aaaaa", "a"}, patternTest{"ababab", "ab"}, patternTest{"abaaba", "aba"}, patternTest{"c.c.c.", "c."}, patternTest{"abcdefg", "abcdefg"}, }
func TestPatterns(t *testing.T){
for _, e := range patternTests{ v := getPattern(e.in) if v != e.out { t.Errorf("getPattern(%s) = %s, but want %s", e.in, v, e.out) } } } |
<리스트 43> 구현 코드 작성 - pattern.go
//pattern.go package pattern
import (
"strings" )
func getPattern(in string) string{
strLen :=len(in) for i:=0; i<(strLen/2); i++ { pattern := in[0:i+1] patternLen := len(pattern) result := strings.Repeat(pattern, strLen/patternLen) if result == in{ return pattern } } return in } |
인터넷에 있는 간단한 프로그래밍 문제를 단위 테스트로 풀어봄으로서 Go언어의 단위 테스트를 경험해 보자. 지금까지의 내용을 이해했다면 코드를 읽는 데 큰 어려움은 없으리라 생각한다.
pattern_test.go에서 테스트 코드를 먼저 작성한다. pattern. go에서 실제 문제를 해결하는 코드를 구현한다. 만약 여러분이 C/C++환경에서 단위 테스트를 위한 도구인 CppUTest나 자바의 JUnit를 사용해 본 적이 있다면 비교해 보는 것도 좋겠다.
마치며
아직도 풀어놓지 못한 Go언어의 특징이 많다. 다른 상세한 특징과 예제들은 Go언어 홈페이지(http://golang.org)에서 찾아볼 수 있다. 지금까지의 내용을 이해했다면 제공하는 문서를 읽고 이해할 수 있는 기본기는 갖춰졌으리라 생각한다
아직도 풀어놓지 못한 Go언어의 특징이 많다. 다른 상세한 특징과 예제들은 Go언어 홈페이지(http://golang.org)에서 찾아볼 수 있다. 지금까지의 내용을 이해했다면 제공하는 문서를 읽고 이해할 수 있는 기본기는 갖춰졌으리라 생각한다
No comments:
Post a Comment