모든 GO 프로그램은 패키지로 이루어져있음.
프로그램은 main 패키지에서 실행된다.
패키지 사용은 import "패키지명"
으로 사용할 수 있다. 예를 들어 import "fmt" 는 package fmt 문으로 시작되는 파일들로 구성되어 있는 파일을 사용하는 것이다.
import "fmt"
import "math"
와 같이 단일 임포트를 여러 번 작성하거나
import (
"fmt"
"math"
)
와 같이 그룹 임포트로 여러 패키지를 사용할 수 있다.
전자보다 후자의 스타일로 작성하는 것을 권장한다.
GO 에서는 대문자로 시작하는 이름이 export 된다.
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
위 상황은 Pi
가 math
패키지에서 export 되었다는 것을 알 수 있다.
Pi 가 아닌 pi 와 같이 소문자로 시작되었다면 export 되지 않는다.
함수는 0개 이상의 인자를 받을 수 있다.
다음은 2개의 int 형 매개변수를 이용한다.
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
변수 이름 뒤에 type
이 온다는 것을 명심하자.
(포인터와 함수가 복잡함에 따라 왼쪽에서 오른쪽으로 읽는 것이 어렵고 명확하지 않아 오른쪽에서 왼쪽으로 읽는 구문을 도입)
두 개 이상의 연속된 매개변수가 같은 type일 때는 마지막 변수를 제외한 매개변수들의 type을 생략할 수 있음.
x int, y int ➡ x, y int
GO 언어 특징 중 하나는 함수가 여러 값을 반환할 수 있다는 것이다.
package main
import "fmt"
func swap(x, y string) (string, string) {
return x, y
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
한 함수는 몇 개의 결과든 반환할 수 있다.
위 함수 swap 의 결과는 "hellow" 와 "world" 가 합쳐져 "hellow world" 를 반환한다.
GO 함수에선 반환 인자나 결과 인자에 이름을 부여하여 일반 변수처럼 사용할 수 있다. 이름을 부여하면 함수 시작 시 해당 타입의 제로 값으로 초기화된다.
함수가 인자 없이 반환문을 수행할 경우엔 결과 매개변수의 현재 값이 반환 값으로 사용된다. 이를 naked return 이라고 한다.
이름 부여는 필수가 아니지만 이름 부여는 코드를 더 짧고 명확하게 만들며, 문서화가 된다.
그리고 이름 있는 결과는 초기화되고 아무 내용 없이 반환되어 명확해질 뿐만 아니라 단순해질 수 있다.
GO 의 defer
문은 자신이 포함하는 함수 내에서 함수가 반환되기 전에 실행하도록 예약한다. (연기된 함수라고 볼 수 있음)
대표적으로는 파일 스트림을 닫는 것이다.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // Contents 함수가 리턴되기 전에 실행
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err // 여기서 리턴되면 f 는 닫힘
}
}
return string(result), nil // 여기서 리턴되도 f 는 닫힘
}
위와 같이 Close 함수를 지연시키면 두 가지 장점을 얻게 된다.
open
함수 근처에 close
함수가 위치하면 명확히 확인이 가능defer
함수의 첫 번째 특징으로는 LIFO 순서에 의하여 실행된다는 것이다.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
위 코드의 실행 결과는 4 3 2 1 0 을 출력할 것이다.
두 번째 특징으로는 매개변수가 함수일 경우엔 defer 함수가 호출될 때가 아닌 실행될 때 평가된다.
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
위 함수는 아래와 같은 결과물을 출력
entering: b in b entering: a in a leaving: a leaving: b
각 defer 함수의 매개변수는 그 자리에서 바로 평가되어 실행되는 것을 확인할 수 있다.
GO 의 기본 type 은 다음과 같다.
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // unit8 별칭
rune // int32 별칭
// 유니코드에서 code point 를 의미
float32 float64
complex64 complex128
몇 가지 변수 선언의 예시를 보자.
변수 선언은 import 문과 같이 조각으로 쪼개어 선언을 할 수 있다.
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
명시적 초깃값 없이 선언된 변수는 그 타입의 zero value가 주어진다.
숫자 타입은 0
boolean 타입은 false
string 에는 빈문자열 ( "" )
변수 선언은 한 변수 당 하나의 초깃값을 포함할 수 있다.
만약 초깃값이 존재한다면 type 을 생략할 수 있다. 그리고 이 경우에는 초깃값의 type 을 취한다.
var i, j int = 1, 2 var c, python, java = true, false, "no!"
함수 내에서는 :=
이란 선언을 통해 암시적 type 으로 var 처럼 사용할 수 있다.
함수 밖에서는 모든 선언이 키워드(var, func 등)로 시작하기 때문에 :=
구문을 사용할 수 없다.
type(v)
는 v 라는 값을 해당 type 으로 변환시켜준다.
package main
import (
"fmt"
)
func main() {
var f float64 = 5
var z uint = uint(f)
fmt.Println(z)
}
위 코드에서 f 변수를 z 변수에 타입 변환하여 담아내는 모습이다.
var z uint = uint(f)
부분에서 uint(f) 로 타입 변환을 하지 않는다면 에러가 발생하는 것을 확인할 수 있다.
:=
혹은 var =
표현으로 명시적인 type 을 정의하지 않는 다면 값으로부터 유추되어 선언된다.
var i int j := i // j 는 int
하지만, 오른 편에 type 으로 정해지지 않은 숫자 상수가 올 시에는 상수의 정확도에 따라 정의된다.
i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128
GO 의 초기화는 C 나 C++ 보다 더 강력하다.
복잡한 구조체를 생성하거나 초기화되는 객체들 간의 순서를 정하는 문제도 정확히 처리할 수 있다. 이는 심지어 다른 패키지 사이에서도 작동한다.
상수는 변수처럼 선언되지만 const
키워드와 함께 선언된다.
대신 :=
를 통해서는 선언될 수 없다.
숫자형 상수는 정확한 값이어야 하며 type 이 정해지지 않은 상수는 그것의 문맥에서 필요한 type 을 취한다.
다음 실행문을 확인해보자.
package main
import "fmt"
const (
// 1 비트를 왼쪽으로 100번 이동시켜 큰 숫자를 만듦
Big = 1 << 100
// 비트를 다시 오른쪽으로 99번 이동시켜 작은 수를 만듦
Small = Big >> 99
)
func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
return x * 0.1
}
func main() {
fmt.Println(needInt(Small))
fmt.Println(needFloat(Small))
fmt.Println(needFloat(Big))
}
실행 결과는 다음과 같다.
21 0.2 1.2676506002282295e+29
위에서 볼 수 있듯 문맥에서 필요한 type 을 취하는 것을 볼 수 있다.
needInt(Big)
을 출력하면 int 형보다 크기 때문에 에러가 발생할 것이다.
지역적으로 정의된 상수도 컴파일할 때 생성되며 숫자, 문자, 문자열, 참/거짓 중의 하나가 되어야 한다.
상수를 정의하는 표현식은 컴파일러가 컴파일 시점에서 실행 가능한 상수 표현식이어야 한다. 예로 1 << 3
은 상수 표현식이지만 math.Sin(math.Pi/4)
는 상수 표현식이 아니다.
math 패키지의 Sin 함수에 대한 호출이 런타임 시에만 가능하기 때문이다.
상수도 앞서 var ( ) 형식으로 쪼개어 선언할 수 있다.
const ( i int = 1 j int = 2 )
상수 표현식만 가능하기 때문에 값이 증가하는 것을 반복하기 위해서는 일일이 타이핑을 해야할 수도 있다.
const ( KB float64 = 1 << (10 * 1) MB float64 = 1 << (10 * 2) GB float64 = 1 << (10 * 3) )
추가적으로 다음과 같이 첫 번째 선언 후의 상수의 선언을 생략하면 첫 번째 선언을 따르게 된다.
const ( i int = 1 j )
이를 이용하여 열거형(enum) 상수를 쉽게 만들 수 있는데 iota
라는 열거자를 이용하여 생성한다.
iota
는 0, 1, 2 ... 으로 연속적인 형식화되지 않은 정수 상수를 나타낸다.
이를 이용하여 다음과 같이 쉽게 사용할 수 있다.
const (
_ = iota // 공백 식별자를 이용해서 값인 0을 무시
KB float64 = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
변수의 초기화는 상수와 같은 방식이지만, 런타임에 계산되는 일반 표현식도 가능하다.
var (
i = add(1, 2)
j = sub(3, 1)
)
각 소스 파일은 상태를 셋업하기 위해 init
함수를 정의할 수 있다. (매개변수를 가지지 않고, 각 파일은 여러 init 함수를 가질 수 있음)
init
함수는 모든 임포트된 패키지들이 초기화되고 패키지 내의 모든 변수 선언이 평가된 이후에 호출된다.
선언 형태로 표현할 수 없는 초기화 외에도, 실제 프로그램의 실행이 일어나기 전에 프로그램의 상태를 검증하고 올바르게 복구하는데 자주 사용된다.
예시
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}