Skip to content

Latest commit

 

History

History
588 lines (427 loc) · 23.7 KB

5-20.md

File metadata and controls

588 lines (427 loc) · 23.7 KB

서문

서문

2019년 8월, 1년쯤 전 Ultimate Go Study Guide 라는 프로젝트를 GitHub 에 공유 하였습니다. 그리고 놀랍게도, 커뮤니티의 많은 관심을 받았으며 2020년 8월 기준 12K star, 900 fork 를 넘어섰습니다. 20 여분의 contributor 분들 덕분입니다.

프로젝트는 Ardan Lab's Ultimate Go course 를 공부하며 정리한 것 입니다. Bill Kennedy와 Ardan Labs team이 이처럼 멋진 코스를 오픈소스화 한 것에 한 없는 감사를 드립니다. 지식과 통찰을 코스에 녹여내고, 모두에게 나누어 준 엄청난 작업이었습니다.

사람마다 나름의 학습 방법이 있겠지만, 저는 예제를 따라해보고 실행하며 배웁니다. 신중히 노트하고, 소스코드에 바로 코멘트하여 코드 한 줄, 한 줄을 확실히 이해하고 코드 뒤에 숨어있는 이론까지 신경씁니다.

Ultimate Go Study Guide가 성장하며 많은 분들이 전자책 버전을 요청하셨습니다. 이어서 읽을 수 있고, 좀더 편하게 읽을 수 있기 때문입니다.

그래서 이렇게 The Ultimate Go Study Guide eBook version을 만들었습니다. 지난 3개월 여 제 여유시간 대부분을 Ultimate Go Study Guide를 200 페이지의 책으로 만드는데 쏟아부었습니다. Ultimate Go 의 모든 좋은 점에 더하여, 전자책에서는 두 가지 장점이 더해졌습니다.

  • code 의 input과 output을 따라가며 썼습니다.
  • 다이어그램이 추가되었습니다.

전자책 버전을 통해 Go를 좀더 쉽게 배우셨으면 합니다. 다시 한 번 모든 분들의 지원과 성원에 감사합니다. 정말 감사합니다.

즐겁게 읽으십시오!

Go 언어의 역학적 고찰(Language Mechanics)

문법

변수

빌트인 타입

타입은 두 가지 질문을 통해 완전성과 가독성을 제공한다

  • 할당한 메모리의 크기는 얼마인가? (예. 32-bit, 64-bit)
  • 이 메모리는 무엇을 의미하는가? (예. int, uint, bool, ...)

타입은 int32, int64처럼 명확한 이름도 있다. 예를 들어

  • uint8은 1 바이트 메모리에 10진수 숫자를 가지고 있다.
  • int32는 4 바이트 메모리에 10진수 숫자를 가지고 있다.

uintint 처럼 메모리 크기가 명확하지 않은 타입을 선언하면, 아키텍처에 따라 크기가 달라진다. 64-bit OS라면, intint64와 같은 크기가 되고, 32-bit OS 라면 int32와 같은 크기가 된다.

워드 크기

워드의 크기는 워드가 몇 바이트인지를 말하며, 이는 메모리 주소의 크기와 같다. 예를 들어 64 비트 아키텍처에서 워드 사이즈는 64 비트(8 바이트)이고, 메모리 주소의 크기도 64 비트이다. 따라서 int 는 64 비트이다.

제로값 개념

모든 변수는 초기화되어야 한다. 어떤 값으로 초기화할지를 명시하지 않으면, 제로값으로 초기화 된다. 할당하는 메모리의 모든 비트는 0으로 리셋된다.

Type Zero value
Boolean false
Integer 0
Floating Point 0
Complex 0i
String ""
Pointer nil

선언과 초기화

var 로 변수를 선언하면 타입의 제로값으로 초기화된다.

var a int
var b string
var c float64
var d bool
fmt.Printf("var a int \t %T [%v]\n", a, a)
fmt.Printf("var b string \t %T [%v]\n", b, b)
fmt.Printf("var c float64 \t %T [%v]\n", c, c)
fmt.Printf("var d bool \t %T [%v]\n\n", d, d)
var a int     int [0]
var b string  string []
var c float64 float64 [0]
var d bool    bool [false]

문자열은 uint8 타입의 연속이다

문자열은 두 개의 워드로 된 데이터 구조체이다. 첫 번째 워드는 뒤에 숨겨져 있는 배열을 가리키는 포인터이고, 두 번째 워드는 문자열의 길이이다. 문자열의 제로값은 첫 번째 워드는 nil, 두 번째 워드는 0이다.

짧은 변수 선언(short variable declaration) 연산자를 사용하면 선언과 동시에 초기화 할 수 있다. (역자 주. 제로 값이 아닌 특정한 값으로 초기화 하려 할때 자주 쓴다.)

aa := 10
bb := "hello" // 첫 번째 워드는 문자들의 배열을 기리키는 포인터이고, 두 번째 워드는 5이다.
cc := 3.14159
dd := true

fmt.Printf("aa := 10 \t %T [%v]\n", aa, aa)
fmt.Printf("bb := \"hello\" \t %T [%v]\n", bb, bb)
fmt.Printf("cc := 3.14159 \t %T [%v]\n", cc, cc)
fmt.Printf("dd := true \t %T [%v]\n\n", dd, dd)
aa := 10      int [10]
bb := "hello" string [hello]
cc := 3.14159 float64 [3.14159]
dd := true    bool [true]

변환과 타입 변경(Conversion vs casting)

Go 는 casting을 지원하지 않고 conversion을 지원한다. 컴파일러가 컴파일 할때에 메모리가 더 있는 듯 처리하기보다 실제로 메모리를 더 할당한다.

aaa := int32(10)
fmt.Printf("aaa := int32(10) %T [%v]\n", aaa, aaa)
aaa := int32(10) int32 [10]

구조체

example 구조체 타입은 다른 타입의 필드들을 가지고 있다.

type example struct {
    flag    bool
    counter int16
    pi      float32
}

선언과 초기화(Declare and initialize)

example 구조체 타입의 변수를 선언하면, 구조체의 필드들은 제로값으로 초기화된다.

var e1 example

fmt.Printf("%+v\n", e1)
{flag:false counter:0 pi:0}

`example 구조체에 할당하는 메모리의 크기는 얼마일까?

bool은 1 바이트, int16은 2 바이트, float32는 4바이트이다. 모두 7바이트이지만, 실제로는 8바이트를 할당한다. 이를 이해하려면 패딩(padding)정렬(alignment)을 알아야 한다. 패딩 바이트는 boolint16 사이에 위치한다. 정렬 때문이다.

정렬: 하드웨어에게는 정렬 경계(alignment boundary)내의 메모리를 읽게 하는 것이 효율적이다. 하드웨어가 정렬 경계에 맞춰 읽게 소프트웨어에서 챙겨주는 것이 정렬이다.

규칙 1:

특정 값의 메모리 크기에 따라 Go는 어떤 정렬이 필요할지 결정한다. 모든 2 바이트 크기의 값은 2 바이트 경계를 가진다. bool값은 1 바이트라서 주소 0번지에서 시작한다. 그러면 다음 int16은 2번지에서 시작해야 한다. 건너뛰게 되는 1 바이트에 패딩 1 바이트가 들어간다. 만약 int16이 아니라 int32라면 3 바이트의 패딩이 들어간다.

규칙 2:

가장 큰 메모리 사이즈의 필드가 전체 구조체의 패딩을 결정한다. 가능한 패딩이 적을 수록 좋은데 그러려면 큰 필드부터 가장 작은 필드의 순서로 위치시키는 것이 좋다. example 구조체를 아래와 같이 정의하면 전체 구조체의 사이즈는 8 바이트를 따르게 되는데 int64가 8 바이트이기 때문이다.

type example struct {
    counter int64
    pi      float32
    flag    bool
}

example 타입의 변수를 선언하고 구조체 리터럴로 초기화 하였다. 이때 각 라인은 콤마(,)로 끝나야 한다.

e2 := example{
    flag:    true,
    counter: 10,
    pi:      3.141592,
}
fmt.Println("Flag", e2.flag)
fmt.Println("Counter", e2.counter)
fmt.Println("Pi", e2.pi)
Counter 10
Pi 3.141592
Flag true

익명의 타입 변수를 선언하고, 구조체 리터럴로 초기화 할 수 있다. 익명 타입은 재사용할 수 없다.

e3 := struct {
    flag    bool
    counter int16
    pi      float32
}{
    flag:    true,
    counter: 10,
    pi:      3.141592,
}
fmt.Println("Flag", e3.flag)
fmt.Println("Counter", e3.counter)
fmt.Println("Pi", e3.pi)
Flag true
Counter 10
Pi 3.141592

이름이 있는 타입과 익명 타입(Name type vs anonymous type)

두 구조체 타입의 필드가 완전히 같다 해도, 한 타입의 구조체 변수를 다른 타입의 구조체 변수에 대입할 수는 없다. 예를 들어 example1, example2가 동일한 필드를 가지는 구조체 타입이라 할 때에, var ex1 example1, var ex2 example2라고 변수를 선언하더라도 ex1 = ex2라는 대입은 허용되지 않는다. ex1 = example1(ex2) 라고 명시적인 변환(conversion)을 해줘야 한다. 하지만 만약 ex가, 위의 ex3 변수처럼, 동일한 구조의 익명 구조체 타입이라면 ex1 = ex 는 가능하다.

var e4 example
e4 = e3
fmt.Printf("%+v\n", e4)
{flag:true counter:10 pi:3.141592}

포인터

항상 값을 전달한다

포인터는 오직 한가지 목적을 가지고 있다: 공유. 프로그램의 경계를 가로질러 값을 공유하는 것이다. 여러 종류의 프로그램 경계가 있는데, 가장 흔한 것은 함수 호출이다. 고루틴 사이에도 경계가 있을 수 있다. 이에 대해서는 나중에 다루도록 한다.

프로그램이 시작할 때, 런타임은 고루틴을 생성한다. 모든 고루틴은 분리된 수행 경로이며 각각의 수행 경로는 머신이 수행해야 할 명령을 가지고 있다. 고루틴을 경량의 쓰레드라 생각해도 된다. go 키워드로 고루틴을 생성하지 않는 간단한 프로그램도 하나의 고루틴은 가진다: main 고루틴이다.

모든 고루틴은 스택이라 부르는 메모리 블럭을 할당받는데 크기는 2 킬로바이트로 매우 작다. 하지만 크기는 필요에 따라 변할 수 있다. 함수를 호출하면 수행을 위해 스택을 사용한다. 스택은 아래쪽으로 증가한다.

모든 함수는 스택 프레임을 가지는데 함수의 메모리 수행을 의미한다. [재방문] 모든 스택 프레임의 크기는 컴파일을 할 때에 알 수 있다. 컴파일러가 크기를 알 수 없는 값이 스택에 자리잡을 수는 없다. 그건 힙에 저장해야 한다.

제로값(zero value) 덕분에 우리는 모든 스택 프레임을 초기화 할 수 있다. 스택은 알아서 정리(cleaning) 되며, 그 방향은 아래쪽이다. 함수를 만들때마다 제로값으로 스택 프레임을 초기화하며 정리한다. [재방문] 메모리를 떠날때는 다시 필요하게 될지 모르기 때문에 위쪽으로 떠난다.

값의 전달(Pass by value)

int 타입의 변수를 초기값 10으로 선언하면 이 변수는 스택에 저장된다.

count := 10
// 변수의 주소를 얻기 위해 &를 사용한다.
fmt.Println("count:\tValue Of[" , count, "]\tAddr Of[" , &count, "]")

// count의 값을 전달한다.
increment1(count)

// increment1 를 실행한 다음의 count 값을 출력한다. 바뀐 것이 없다.
fmt.Println("count:\tValue Of[" , count, "]\tAddr Of[" , &count, "]")

// count의 주소를 전달한다. 이것 역시 "pass by value", 즉, 값을 전달하는 것이다.
// "pass by reference" 가 아니다. 주소 역시 값인 것이다.
increment2(&count)

// increment2 를 실행한 다음 count 값을 출력한다. 값이 변경되었다.
fmt.Println("count:\tValue Of[" , count, "]\tAddr Of[" , &count, "]")

func increment1(inc int) {
    // inc 의 값을 증가 시킨다.
    inc++
    fmt.Println( "inc1:\tValue Of[" , inc, "]\tAddr Of[" , &inc, "]")
}

// increment2 는 inc를 포인터 변수로 선언했다. 이 변수는 주소값을 가지며, int 타입의 값을 가리킨다.
// *는 연산자가 아니라 타입 이름의 일부이다. 이미 선언된 타입이건, 당신이 선언한 타입이건
// 모든 타입은 선언이 되면 포인터 타입도 가지게 된다.
func increment2(inc *int) {
    // inc 포인터 변수가 가리키고 있는 int 변수의 값을 증가시킨다.
    // 여기서 *는 연산자이며 포인터 변수가 가리키고 있는 값을 의미한다.
    *inc++
    fmt.Println("inc2:\tValue Of[" , inc, "]\tAddr Of[" , &inc, "]\tValue Points To[" , *inc, "]")
}
count: Value Of[ 10 ] Addr Of[ 0xc000050738 ]
inc1:  Value Of[ 11 ] Addr Of[ 0xc000050730 ]
count: Value Of[ 10 ] Addr Of[ 0xc000050738 ]
inc2:  Value Of[ 0xc000050738 ] Addr Of[ 0xc000050748 ] Value Points To[ 11 ]

이스케이프 분석(Escape analysis)

변수 ustayOnStack 함수에서 벗어나지 못한다. 함수 바깥에서 쓸 수 없다는 말이다. 컴파일 할 때에 u의 크기를 알 수 있기에 컴파일러는 u를 스택 프레임에 저장한다.

// user는 시스템의 user를 의미한다.
type user struct {
    name  string
    email string
}

func stayOnStack() user {
    // 스택 프레임에 변수를 생성하고 초기화한다.
    u := user{
      name:  "Hoanh An",
      email: "hoanhan101@gmail.com",
    }

    // 값을 리턴하여 main의 스택 프레임으로 전달한다.
    return u
}

escapeToHeap 에서는 변수가 함수 바깥으로 나온다. 구현상으로는 stayOnStack 함수와 거의 같아 보인다. user 타입의 변수를 생성하고 초기화한다. 하지만 미묘한 차이가 하나 있다. 값을 리턴하는 것이 아니라 값의 주소를 리턴한다. 주소값을 콜 스택으로 전달하는 것이다. 우리는 포인터 개념(pointer semantics)을 사용하고 있다.

main 함수가 호출한 함수의 스택 프레임에 존재하는 변수의 포인터를 리턴 받는 것 처럼 보일 수 있다. 스택 프레임은 재사용이 가능한 메모리이며, 언제든 escapeToHeap 함수를 호출하면 스택 프레임을 재할당하고 초기화한다. 만약 그렇다면 이건 문제다.

제로값에 대해 잠시 생각해보자. 모든 스택 프레임은 함수 호출시에 제로값으로 초기화되고, 스택은 알아서 아래 방향으로 정리된다. 함수를 호출할 때마다 제로값으로 정리되는 것이다. 다시 필요하게 될지 모르기에 메모리를 떠날 때는 위쪽으로 떠난다.

예제로 돌아가보자. 변수 u의 주소값을 main의 콜 스택에 전달하는 것처럼 보이는데 그렇다면 이 주소의 메모리는 언제 지워질 지 모르는 것이다. 하지만 다행이도 실제 작동은 그렇지 않다.

실제로는 이스케이프 분석이 이루어진다. return &u 라인 덕분에 함수를 위한 스택 프레임이 아닌 힙에 저장이 되는 것이다.

이스케이프 분석은 무엇을 스택에 둘지 힙에 둘지를 결정한다. stayOnStack 함수에서는 값의 복사본을 전달하기에 스택 프레임에 두어도 된다. 하지만 우리가 콜 스택 위쪽으로 무언가를 공유할 때는, 이스케이프 분석은 힙에 저장하도록 명령한다. main 함수는 결국 힙 메모리를 가리키는 포인터를 가지게 된다. 사실 힙 메모리 할당은 즉시 이루어진다. escapeToHeap은 힙을 가리키는 포인터를 가지고 있는 것이다. 하지만 u는 값 개념(value semantics)을 기반으로 하게 된다.

func escapeToHeap() *user {
    u := user{
        name:  "Hoanh An",
        email: "hoanhan101@gmail.com",
    }

    return &u
}

스택 공간이 부족해지면 어떻게 될까?

함수 호출을 하면, 가장 먼저 이 프레임을 위한 스택 공간이 충분한가? 를 확인한다. 충분하면 아무 문제가 없지만, 부족하다면 더 큰 스택 프레임을 만든 다음, 값을 복사해야 한다. 스택 공간을 조금만 주어서, 공간이 부족할 때 마다 값을 복사해야 하는 것은 상충관계가 있지만 고루틴에 스택 메모리를 적게 할당하여 얻는 이점이 더 크다.

스택은 커질 수 있기 때문에 하나의 고루틴이 다른 고루틴의 스택 메모리에 대한 포인터를 가질 수 없다. 컴파일러가 모든 포인터를 추적하는 것은 지나친 과부하가 되어 지연 시간이 엄청나게 늘어날 수 있다.

따라서, 하나의 고루틴의 스택은 온전히 그 고루틴만을 위한 것이고, 고루틴 사이에 공유하지 않는다.

가비지 컬렉션

힙에 저장한다는 것은 가비지 컬렉션이 개입한다는 것이다. 가비지 컬렉터(GC)에 있어서 가장 중요한 것은 페이싱 알고리즘(pacing algorithm)이다. 최소한의 가비지 컬렉션 작업시간 t가 소요되도록 어떠한 주기와 페이스로 가비지 컬렉션을 실행할 지를 결정해야 한다.

4 MB 힙을 가진 프로그램이 있다고 할 때에, GC는 라이브 힙(live heap)을 2 MB로 유지하려 한다. 라이브 힙이 4 MB를 넘어서면 더 큰 힙을 할당해줘야 한다. GC의 페이스는 힙이 얼마나 빠르게 커지는지에 달려있다. 그에 맞게 적절하게 라이브 힙을 줄여줘야 하는 것이다.

GC가 작동할 때는 성능이 떨어질 수밖에 없다. 그래야 모든 고루틴이 동시에 작동할 수 있다. GC 역시 가비지 컬렉션 작업을 하는 고루틴들을 실행시키며, 가용 CPU 의 25%를 사용한다. GC와 페이스 알고리즘에 대한 자세한 설명은 링크를 참조바란다: !Go 1.5 concurrent garbage collector pacing

함수

// user 구조체는 user 정보를 담고 있다.
type user struct {
    ID   int
    Name string
}

// updateStats 구조체는 업데이트 정보를 담고 있다.
type updateStats struct {
    Modified int
    Duration float64
    Success  bool
    Message  string
}

func main() {
    // user 프로필을 가져온다.
    u, err := retrieveUser("Hoanh")
    if err != nil {
        fmt.Println(err)
        return
    }

    // user 프로필을 보여준다. `u`는 주소값이기에 *를 사용하여 값을 얻어낸다.
    fmt.Printf("%+v\n" , *u)

    // user 의 name 을 업데이트 한다.
    // _(blank identifier)를 사용하여 리턴된 updateStats는 무시하며
    // if 범위 밖에서 사용할 값은 없으니 간결한 문법을 사용하였다.
    if _, err := updateUser(u); err != nil {
        fmt.Println(err)
        return
    }

    // 업데이트가 성공했다고 출력한다.
    fmt.Println("Updated user record for ID", u.ID)
}

retrieveUser는 특정한 사용자의 문서를 가져온다. 문자열 타입의 name을 넣어주면, user 타입을 가리키는 값과 error 타입의 값을 리턴한다.

func retrieveUser(name string) (*user, error) {
    // getUser 함수를 호출하여 JSON 형식의 user를 전달 받는다.
    r, err := getUser(name)
    if err != nil {
        return nil, err
    }

    // JSON 값을 unmarshal하여 저장할 user 타입인 변수 u를 생성한다.
    var u user

    // 변수 u를 json.Unmarshal 함수에 전달하면, 함수는 r로부터 JSON 을 읽어서 변수 u에 넣어준다.
    err = json.Unmarshal([]byte(r), &u)

    // retrieveUser 함수를 호출한 함수에게 u값을 전달한다. 이처럼 retrieveUser 함수에서
    // 생성한 변수의 주소값을 호출한 함수에게 전달하기에 이 변수는 힙 메모리에 할당된다.
    return &u, err
}

getUser함수는 웹으로 호출하였을때 특정한 사용자에 대한 JSON 으로 응답이 돌아오는 것을 시뮬레이션 한 것이다.

func getUser(name string) (string, error) {
    response := `{"ID":101, "Name":"Hoanh"}`
    return response, nil
}

updateUser 함수는 특정 사용자가 업데이트 되었다는 응답을 시뮬레이션 한 것이다.

func updateUser(u *user) (*updateStats, error) {
    // response 변수는 JSON 응답을 시뮬레이션 한 것이다.
    response := `{"Modified":1, "Duration":0.005, "Success" : true, "Message": "updated"}`

    // JSON 문서를 userStats 구조체 타입의 변수로 unmarshal한다.
    var us updateStats
    if err := json.Unmarshal([]byte(response), &us); err != nil {
        return nil, err
    }

    // update 성공여부를 확인한다.
    if us.Success != true {
        return nil, errors.New(us.Message)
    }

    return &us, nil
}
{ID:101 Name:Hoanh}
Updated user record for ID 101

상수

상수는 변수가 아니며 (변수들의 타입시스템에 상응하는) 상수만을 위한 타입시스템이 있다. 상수의 최소 정밀도(minimum precision)는 265 bit 이며, 이 정도의 정밀도는 수학적으로 정확하다고 간주한다. 상수는 컴파일을 하는 동안만 존재한다.

선언과 초기화

상수는 타입이 있을 수도, 없을 수도 있다. 타입이 없을 때에는(untyped) 이를 kind로 간주한다. 타입이 없는 상수는 컴파일러가 암묵적으로 특정 타입으로 변환한다.

타입이 없는 상수.

const ui = 12345    // kind: integer
const uf = 3.141592 // kind: floating-point

타입이 있는 상수는 여전히 상수 타입 시스템을 사용하지만 그 정밀도는 타입이 없는 상수에 비해 제한적이다.

const ti int = 12345        // type: int
const tf float64 = 3.141592 // type: float64

상수 1000을 uint8 에 대입하려 하면 오버플로우가 발생한다.

const myUint8 uint8 = 1000

상수는 다른 kind를 산술적으로 지원한다. Kind 승급(kind promotion)을 이용해서 어떤 kind 인지를 결정한다. 이 모든 것은 암묵적으로 이루어진다.

answer 변수는 float64 타입이 될 것이다.

var answer = 3 * 0.333 // KindFloat(3) * KindFloat(0.333)
fmt.Println(answer)
0.999

상수 thirdkind는 실수가 될 것이다.

const third = 1 / 3.0 // KindFloat(1) / KindFloat(3.0)
fmt.Println(third)
0.3333333333333333

상수 zerokind는 정수이다.

const zero = 1 / 3 // KindInt(1) / KindInt(3)
fmt.Println(zero)
0

타입이 있는 상수와 타입이 없는 상수의 산술 계산을 보자. 계산을 하려면 둘은 비슷한 타입이어야 한다. 아래 코드에서는 둘 다 정수이다.

const one int8 = 1
const two = 2 * one // int8(2) * int8(1)

fmt.Println(one)
fmt.Println(two)
1
2

상수 maxInt는 64 bit 아키텍처에서 가장 큰 정수이다.

const maxInt = 9223372036854775807
fmt.Println(maxInt)
9223372036854775807

bigger 상수는 int64 타입보다 훨씬 큰 숫자이지만 타입이 없는 상수이기에 컴파일에 문제가 없다. (아키텍처에 따라 다르긴 하지만) 256 bit 는 정말 큰 공간이다.

const bigger = 9223372036854775808543522345

하지만 biggerInt 상수는 int64 타입이기 때문에 컴파일 시에 에러가 난다.

const biggerInt int64 = 9223372036854775808543522345

iota

const (
    A1 = iota // 0 : 0에서 시작한다
    B1 = iota // 1 : 1 증가한다
    C1 = iota // 2 : 1 증가한다
)

fmt.Println("1:", A1, B1, C1)

const (
    A2 = iota // 0 : 0에서 시작한다
    B2        // 1 : 1 증가한다
    C2        // 2 : 1 증가한다
)

fmt.Println("2:", A2, B2, C2)

const (
    A3 = iota + 1 // 1 : 1에서 시작한다
    B3            // 2 : 1 증가한다
    C3            // 3 : 1 증가한다
)

fmt.Println("3:", A3, B3, C3)

const (
    Ldate= 1 << iota //  1 : 오른쪽으로 0번 시프트 된다. 0000 0001
    Ltime            //  2 : 오른쪽으로 1번 시프트 된다. 0000 0010
    Lmicroseconds    //  4 : 오른쪽으로 2번 시프트 된다. 0000 0100
    Llongfile        //  8 : 오른쪽으로 3번 시프트 된다. 0000 1000
    Lshortfile       // 16 : 오른쪽으로 4번 시프트 된다. 0001 0000
    LUTC             // 32 : 오른쪽으로 5번 시프트 된다. 0010 0000
)
fmt.Println("Log:", Ldate, Ltime, Lmicroseconds, Llongfile, Lshortfile, LUTC)
1: 0 1 2
2: 0 1 2
3: 1 2 3
Log: 1 2 4 8 16 32