[GO] #2-2. 고랭 기본문법 (패키지, 함수, 변수)

Study·2021년 5월 17일
0

고랭

목록 보기
3/18
post-thumbnail

패키지

모든 GO 프로그램은 패키지로 이루어져있음.

프로그램은 main 패키지에서 실행된다.

1. Import

패키지 사용은 import "패키지명" 으로 사용할 수 있다. 예를 들어 import "fmt" 는 package fmt 문으로 시작되는 파일들로 구성되어 있는 파일을 사용하는 것이다.

import "fmt"
import "math"

와 같이 단일 임포트를 여러 번 작성하거나

import (
    "fmt"
    "math"
)

와 같이 그룹 임포트로 여러 패키지를 사용할 수 있다.
전자보다 후자의 스타일로 작성하는 것을 권장한다.

2. Export

GO 에서는 대문자로 시작하는 이름이 export 된다.

package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println(math.Pi)
}

위 상황은 Pimath 패키지에서 export 되었다는 것을 알 수 있다.
Pi 가 아닌 pi 와 같이 소문자로 시작되었다면 export 되지 않는다.

함수

1. 파라미터

함수는 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이 온다는 것을 명심하자.
(포인터와 함수가 복잡함에 따라 왼쪽에서 오른쪽으로 읽는 것이 어렵고 명확하지 않아 오른쪽에서 왼쪽으로 읽는 구문을 도입)

1-1. 타입 생략

두 개 이상의 연속된 매개변수가 같은 type일 때는 마지막 변수를 제외한 매개변수들의 type을 생략할 수 있음.

x int, y int ➡ x, y int

2. 리턴값

2-1. 다중 반환 값

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" 를 반환한다.

2-2. 이름이 있는 결과 파라미터

GO 함수에선 반환 인자나 결과 인자에 이름을 부여하여 일반 변수처럼 사용할 수 있다. 이름을 부여하면 함수 시작 시 해당 타입의 제로 값으로 초기화된다.

함수가 인자 없이 반환문을 수행할 경우엔 결과 매개변수의 현재 값이 반환 값으로 사용된다. 이를 naked return 이라고 한다.

이름 부여는 필수가 아니지만 이름 부여는 코드를 더 짧고 명확하게 만들며, 문서화가 된다.
그리고 이름 있는 결과는 초기화되고 아무 내용 없이 반환되어 명확해질 뿐만 아니라 단순해질 수 있다.

2-3. Defer

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 함수를 지연시키면 두 가지 장점을 얻게 된다.

  1. 파일을 닫는 것을 잊어버리는 실수를 하지 않는 보장
  2. 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 함수의 매개변수는 그 자리에서 바로 평가되어 실행되는 것을 확인할 수 있다.

변수

1. 기본 자료형

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)
)

1-1. Zero value

명시적 초깃값 없이 선언된 변수는 그 타입의 zero value가 주어진다.

숫자 타입은 0
boolean 타입은 false
string 에는 빈문자열 ( "" )

2. Type

변수 선언은 한 변수 당 하나의 초깃값을 포함할 수 있다.

만약 초깃값이 존재한다면 type 을 생략할 수 있다. 그리고 이 경우에는 초깃값의 type 을 취한다.

var i, j int = 1, 2
var c, python, java = true, false, "no!"

함수 내에서는 := 이란 선언을 통해 암시적 type 으로 var 처럼 사용할 수 있다.

함수 밖에서는 모든 선언이 키워드(var, func 등)로 시작하기 때문에 := 구문을 사용할 수 없다.

2-1. Type 변환

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) 로 타입 변환을 하지 않는다면 에러가 발생하는 것을 확인할 수 있다.

2-2. Inference Type

:= 혹은 var = 표현으로 명시적인 type 을 정의하지 않는 다면 값으로부터 유추되어 선언된다.

var i int
j := i // j 는 int

하지만, 오른 편에 type 으로 정해지지 않은 숫자 상수가 올 시에는 상수의 정확도에 따라 정의된다.

i := 42           // int
f := 3.142        // float64
g := 0.867 + 0.5i // complex128

3. 초기화

GO 의 초기화는 C 나 C++ 보다 더 강력하다.

복잡한 구조체를 생성하거나 초기화되는 객체들 간의 순서를 정하는 문제도 정확히 처리할 수 있다. 이는 심지어 다른 패키지 사이에서도 작동한다.

3-1. 상수 (Constants)

상수는 변수처럼 선언되지만 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
)

3-2. 변수

변수의 초기화는 상수와 같은 방식이지만, 런타임에 계산되는 일반 표현식도 가능하다.

var (
	i = add(1, 2)
	j = sub(3, 1)
)

3-3. init 함수

각 소스 파일은 상태를 셋업하기 위해 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")
}
profile
Study

0개의 댓글