출시·고도화 중
Go 안내서 · 2/6
Go의 문법은 의도적으로 작게 유지됩니다. 키워드가 적고, 같은 일을 하는 방법이 대개 하나뿐입니다. 이 장에서는 패키지와 가져오기, 변수와 상수, 함수, 제어 흐름, 그리고 Go만의 defer를 살펴봅니다.
모든 Go 파일은 package 선언으로 시작합니다. 다른 패키지를 쓰려면 import로 가져오고, 가져온 패키지의 이름을 앞에 붙여 사용합니다. 대문자로 시작하는 이름(Println, Pi)만 패키지 밖에서 보입니다.
package main
import (
"fmt"
"math"
"strings"
)
func main() {
fmt.Println(math.Pi)
fmt.Println(strings.ToUpper("go"))
}쓰지 않는 패키지를 가져오면 컴파일 오류가 납니다. 처음에는 번거롭게 느껴지지만, 코드에 죽은 의존성이 남지 않게 해 줍니다.
변수는 var로 선언하거나, 함수 안에서는 짧은 선언 :=를 씁니다. :=는 오른쪽 값에서 타입을 추론합니다. 값을 주지 않고 선언한 변수는 제로 값으로 초기화됩니다. 숫자는 0, 문자열은 "", 불리언은 false, 포인터·슬라이스·맵·채널·함수·인터페이스는 nil입니다.
var count int // 0
var name = "gopher" // string 으로 추론
x, y := 10, 3.5 // int, float64
x, z := 20, true // 왼쪽에 새 변수(z)가 하나 이상 있으면 := 를 다시 쓸 수 있다
const Limit = 100
const (
Read = 1 << iota // 1
Write // 2
Exec // 4
)iota는 const 묶음 안에서 0부터 하나씩 늘어나는 값으로, 열거형 상수를 만들 때 씁니다. :=는 함수 안에서만 쓸 수 있고, 패키지 수준에서는 var를 써야 합니다. 선언하고 쓰지 않는 지역 변수도 컴파일 오류입니다.
함수는 func 키워드로 정의하며, 매개변수 이름 뒤에 타입을 씁니다. Go 함수는 값을 여러 개 돌려줄 수 있어서, 결과와 오류를 함께 돌려주는 방식이 표준처럼 쓰입니다.
func add(a, b int) int {
return a + b
}
// 결과와 오류를 함께 돌려준다
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("0으로 나눌 수 없습니다")
}
return a / b, nil
}
// 이름 붙은 반환값과 가변 인자
func sum(nums ...int) (total int) {
for _, n := range nums {
total += n
}
return
}
func main() {
q, err := divide(10, 4)
if err != nil {
fmt.Println("오류:", err)
return
}
fmt.Println(q, sum(1, 2, 3))
double := func(n int) int { return n * 2 } // 익명 함수(클로저)
fmt.Println(double(add(1, 2)))
}필요 없는 반환값은 빈 식별자 _로 버립니다. 함수는 값이므로 변수에 담거나 다른 함수에 넘길 수 있고, 바깥 변수를 붙잡는 클로저도 만들 수 있습니다.
Go의 조건문과 반복문은 괄호를 쓰지 않지만 중괄호는 반드시 씁니다. 반복문은 for 하나뿐이며, 쓰는 방식에 따라 다른 언어의 while이나 무한 반복 역할도 합니다.
// if 에는 짧은 초기화 문을 둘 수 있다
if n, err := strconv.Atoi("42"); err == nil {
fmt.Println(n)
}
for i := 0; i < 3; i++ { // 전통적인 세 부분 for
fmt.Println(i)
}
n := 1
for n < 100 { // while 처럼
n *= 2
}
for i, s := range []string{"a", "b"} { // 슬라이스 순회
fmt.Println(i, s)
}
for i := range 3 { // 정수 범위 순회: 0, 1, 2
fmt.Println(i)
}
switch day := "sat"; day {
case "sat", "sun":
fmt.Println("주말")
default:
fmt.Println("평일")
}switch의 각 case는 자동으로 끝나므로 break가 필요 없습니다. 다음 case로 넘어가야 할 때만 fallthrough를 씁니다. 조건 없이 switch { case x > 0: ... }처럼 쓰면 긴 if-else 사슬을 깔끔하게 바꿀 수 있습니다. 반복을 빠져나가려면 break, 다음 차례로 넘어가려면 continue를 씁니다.
defer는 함수 호출을 감싼 함수가 끝날 때까지 미룹니다. 파일 닫기, 잠금 풀기처럼 정리 작업을 자원을 얻은 바로 옆에 적을 수 있어서 빠뜨릴 걱정이 줄어듭니다. 여러 개를 등록하면 나중에 등록한 것부터(후입선출) 실행됩니다.
func readFirstLine(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // 어떤 경로로 돌아가든 파일이 닫힌다
scanner := bufio.NewScanner(f)
if scanner.Scan() {
return scanner.Text(), nil
}
return "", scanner.Err()
}
func main() {
for i := 0; i < 3; i++ {
defer fmt.Println("defer", i) // 2, 1, 0 순서로 출력
}
}defer에 넘긴 인자는 defer 문을 만나는 순간 평가됩니다. 반복문 안에서 자원을 열고 defer로 닫으면 함수가 끝날 때까지 모두 열린 채로 남으므로, 그럴 때는 반복 본문을 별도 함수로 빼는 편이 좋습니다.
package로 시작하고, 대문자로 시작하는 이름만 밖으로 공개됩니다.:=로 짧게 선언하며, 선언만 한 변수는 제로 값을 가집니다.(결과, error) 형태가 관례입니다.for 하나로 하고, switch는 break 없이 각 case가 끝납니다.defer로 정리 작업을 자원 획득 바로 옆에 적습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.