ytbeom / the-go-programming-language

0 stars 0 forks source link

1. Tutorial #1

Open ytbeom opened 3 years ago

ytbeom commented 3 years ago

1.1. Hello, World

개념서 첫 장에서 빠지면 섭섭한 Hello, world를 출력하는 방법

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}

그 외 언급된 내용들을 쭉 적어보자면

ytbeom commented 3 years ago

1.2. Command-Line Arguments

// echo1은 커맨드라인 인수를 출력한다
package main

import (
    "fmt"
    "os"
)

func main() {
    var s, sep string
    for i := 0; i < len(os.Args); i++ {
        s += sep + os.Args[i]
        sep = " "
    }
    fmt.Println(s)
}

C++로 과제하던 시절 참 많이 쓰던 것들이라 오랜만에 보니 반가웠다. Go에서는 os.Args라는 변수를 사용하는데, 이는 문자열의 슬라이스 형태라고 한다. 슬라이스에 대한 내용은 뒤에서 더 자세히 다룬다고 하니 일단은 pass os.Args[0]은 명령 자체를 의미하기 때문에 커맨드라인으로 들어온 인수들을 받아오려면 Args[1] 부터 Args[len(Args) -1] 까지 접근하면 된다(11번째 줄). var 선언, += 연산자, for문 등 위 코드에 나온 구문들에 대한 설명도 적혀 있지만 항상 그렇듯 뒤에서 더 자세히 언급할 것이기 때문에 굳이 언급하지 않아도 될 것 같다.

// echo2는 커맨드라인 인수를 출력한다
package main

import (
    "fmt"
    "os"
)

func main() {
    s, sep := "", ""
    for _, arg := range os.Args[1:] {
        s += sep + arg
        sep = " "
    }
    fmt.Println(s)
}

이 예제도 작동 결과는 echo1과 동일하다. 다만 range를 사용해 for문의 반복에서 값의 쌍(_, arg)을 만들어내는 예시를 보여주기 위함이라고 한다. 여기서는 인덱스가 필요 없기 때문에 문법상 인덱스의 위치에는 빈 식별자(_)를 사용했다. temp같은 이름으로 지역 변수를 만들고 실제로 사용하지 않는다면 컴파일 오류가 난다고 한다. 까칠하기도 하지...

echo1과 echo2에서는 문자열을 합치기 위해 += 연산을 사용했다.
+= 연산을 사용하면 기존 문자열, 공백 문자(sep), 다음 인자를 결합한 새로운 문자열을 생성하고 s에 대입하는 과정을 거친다. 이 때 s의 이전 값은 더 이상 사용되지 않으므로 이후 GC가 되는 식인데 이 경우 데이터가 많으면 문제가 생길 수 있기에 strings 패키지의 Join 함수를 이용하는 것을 추천한다고 한다.

// echo3은 커맨드라인 인수를 출력한다
package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}
ytbeom commented 3 years ago

1.3. Finding Duplicate Lines

들어온 입력들 중에 중복된 내용과 그 카운트를 출력해주는 프로그램들을 작성하는 예시이다. 첫 번째 예시는 표준입력에서 중복된 내용과 그 카운트를 출력해준다

// dup1은 표준 입력에서 두 번 이상 나타나는 각 줄을
// 앞에 카운트를 추가해 출력한다
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    counts := make(map[string]int)
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: input.Err()에서의 잠재적 오류는 무시한다.
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

여기서 새롭게 등장한 패키지로는 bufio가 있고 main함수 내에서 map을 사용하고 있다. map은 흔히 알려진 map의 개념과 다를 것이 없다. 그리고 %d, %s와 같이 다른 언어에서 자주 쓰는 변환 문자들이 사용되고 있다.

두 번째 예시는 표준입력에서 입력을 받거나, 파일 이름을 커맨드라인 인수로 받아 동일한 동작을 수행한다.

// dup2는 입력에서 두 번 이상 나타나는 각 줄의 카운트와 텍스트를 출력한다.
// 이 프로그램은 표준 입력이나 파일 목록에서 읽는다.
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    counts := make(map[string]int)
    files := os.Args[1:]
    if len(files) == 0 {
        countLines(os.Stdin, counts)
    } else {
        for _, arg := range files {
            f, err := os.Open(arg)
            if err != nil {
                fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
                continue
            }
            countLines(f, counts)
            f.Close()
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

func countLines(f *os.File, counts map[string]int) {
    input := bufio.NewScanner(f)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: input.Err()에서의 잠재적 오류는 무시한다.
}

위와 같이 파일을 작성하고 아래와 같이 txt 파일을 작성해 커맨드라인 인수로 입력하면 image abd, abc, abcc의 개수를 출력해준다.

첫 번째 예시와 두 번째 예시는 표준 입력이나 파일 입력을 한 줄씩 읽어와 처리하는 방식이며 마지막 예시는 한꺼번에 전체 입력을 읽어온 뒤 내용을 처리하는 방식이다.

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

func main() {
    counts := make(map[string]int)
    for _, filename := range os.Args[1:] {
        data, err := ioutil.ReadFile(filename)
        if err != nil {
            fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
            continue
        }
        for _, line := range strings.Split(string(data), "\n") {
            counts[line]++
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

하지만 이 예제에서 문제가 생겼다. 각 파일에서 제일 마지막 줄에 있는 문자열이 제대로 인식되지 않아 결과가 dup2와 다르게 출력된다.... 마지막 조건문에서 n > 1 조건을 지우고 출력해보면 image 처럼 abc지만 abc가 아닌식으로 인식되어 버린 줄이 생겨버린다. 아마 EOF 문자같은 것이 영향을 주는 것 같은데, 인터넷에서 관련 정보를 찾아보다가 지쳐서 일단 마무리... 상당히 기분이 찝찝하다ㅠㅠ

해결했다! %q로 문자열을 출력하면 따옴표를 포함해서 출력해주니 어떤 값들이 들어가는지 확인해보라는 친구의 조언에 따라 %q로 출력해보니 image 이렇게 \r이 포함된 값이 들어가고 있는 것을 알 수 있었다. strings.Split 함수의 인자로 "\n" 대신 "\r\n"을 넣어주니 해결!

ytbeom commented 3 years ago

1.4. Animated GIFs

다른 언어의 개념서들에선 겪어보지 못한 신선함! 첫 챕터에서 GIF 생성법이라니..... 두근두근한다 표준 이미지 패키지의 기본 사용법과 비트맵 이미지의 시퀀스 생성, GIF로 인코딩 등을 다룬다고 한다.

두근두근한 마음은 다음 장 코드를 보고는 싹 사라졌다. 깃에서 코드를 받을 수 있다고 하지만 문법에 익숙해질 겸 따라서 쓰고 있는데 왼손 네번째 손가락이 유난히 아프다ㅠㅠ

// lissajous는 임의의 리사주 형태의 애니메이션 GIF를 생성한다.
package main

import (
    "image"
    "image/color"
    "image/gif"
    "io"
    "math"
    "math/rand"
    "os"
)

var palette = []color.Color{color.White, color.Black}

const (
    whiteIndex = 0 // 팔레트의 첫 번째 색상
    blackIndex = 1 // 팔레트의 다음 색상
)

func main() {
    lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
    const (
        cycles = 5      // x 진동자의 회전수
        res = 0.001     // 회전각
        size = 100      // 이미지 캔버스 크기 [-size..+size]
        nframes = 64    // 애니메이션 프레임 수
        delay = 8       // 10ms 단위의 프레임 간 지연
    )
    freq := rand.Float64() * 3.0 // y 진동자의 상대적 진동수
    anim := gif.GIF{LoopCount: nframes}
    phase := 0.0 // 위상 차이
    for i := 0; i < nframes; i++ {
        rect := image.Rect(0, 0, 2*size+1, 2*size+1)
        img := image.NewPaletted(rect, palette)
        for t := 0.0; t < cycles*2*math.Pi; t += res {
            x := math.Sin(t)
            y := math.Sin(t*freq + phase)
            img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex)
        }
        phase += 0.1
        anim.Delay = append(anim.Delay, delay)
        anim.Image = append(anim.Image, img)
    }
    gif.EncodeAll(out, &anim) // NOTE: 인코딩 오류 무시
}

안된다! gif 파일은 만들어지는데 파일이 열리지 않는다. gopl.io에 올라와 있는 코드로 해도 똑같은 결과가 나오는거 봐선 내 문제가 아닌 것 같다(고 믿고 싶다)

ytbeom commented 3 years ago

1.5. Fetching a URL / 1.6 Fetching URLs Concurrently

두 챕터로 나뉘어져 있는 내용이지만 같이 다루는 편이 좋을 것 같아서 하나로 묶음 인터넷에 있는 정보를 읽어오기 위해서, go에서는 net 이라는 이름의 package를 제공함 아래 fetch 코드는 URL에서 읽어온 정보를 바로 화면에 그려주는 아주 간단한 역할을 수행함

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    for _, url := range os.Args[1:] {
        resp, err := http.Get(url)
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
            os.Exit(1)
        }
        b, err := ioutil.ReadAll(resp.Body)
        resp.Body.Close()
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
            os.Exit(1)
        }
        fmt.Printf("%s", b)
    }
}

http.Get 함수는 HTTP request를 생성하고 error가 없다면 그 결과를 resp에 저장한다. err != nil 이 여러 번 등장해서 nil에 대해서도 간단히 언급하고 지나가려고 한다. 먼저 go에서는 변수를 선언할 때 명시적인 초기값을 할당하지 않으면 해당 타입의 zero value가 자동으로 할당되는 것으로 알고 있다. 예를 들어 정수면 0, 문자열이면 "" 처럼 말이다. nil은 포인터, 인터페이스, 맵, 슬라이스, 채널, 함수의 zero value라고 하니 저 구문을 해석해보면 http.Get(url)의 결과를 resp, err에 할당하는데 err이 zero value가 아니라는 것은 에러가 발생했으니 에러 내용을 출력하고 Exit를 수행하는 정도로 이해가 된다. nil에 대한 언급은 여기까지 하고 마저 코드에 대한 얘기를 해보자면 resp의 body에 있는 내용을 ioutil.ReadAll 함수를 사용해 b 변수에 할당하고 그 내용을 화면에 출력해주는 아주 간단한 코드이다. b는 html 형태로 들어올테니, 뭔가 찾고싶은 태그나 내용이 있다면 저 내용을 파싱하는 식으로 사용할 수 있을 것 같다. 애초에 그게 목적이라면 좀 더 좋은 방법들이 있겠지만....

1.5에서는 하나의 URL의 내용을 읽어오는 코드를 작성했다면 1.6에서는 여러 URL의 내용을 동시에 읽어오는 코드에 대해 소개하고 있다. 8장과 9장에서 goroutine, channel에 대해 더 자세히 다룬다고 한다. 아래의 fetchall 코드는 command line으로 받은 여러 URL에 대해 동시에 fetch 작업을 수행해 가장 오래 fetch에 소요되는 시간이 총 코드 수행 시간과 동일하게끔 작동한다.

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)

func main() {
    start := time.Now()
    ch := make(chan string)
    for _, url := range os.Args[1:] {
        go fetch(url, ch) // goroutine 시작
    }
    for range os.Args[1:] {
        fmt.Println(<-ch) // ch 채널에서 수신
    }
    fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprint(err) // ch 채널로 송신
        return
    }
    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close()
    if err != nil {
        ch <- fmt.Sprintf("while reading %s: %v", url, err)
        return
    }
    secs := time.Since(start).Seconds()
    ch <- fmt.Sprintf("%.2fs\t%7d\t%s", secs, nbytes, url)
}

주석에 달았듯이, 여기서 16번째 줄이 goroutine을 생성해 여러 함수가 동시에 수행되게끔 하는 코드라고 한다. 그리고 그 다음 for loop에서는 ch 채널을 사용해 각 goroutine에서 값을 전달받고 이를 출력한다.

ytbeom commented 3 years ago

1.7. A Web Server

아주아주 간단한 웹서버를 만드는 내용이다. 예제 코드로 server1, server2, server3이 나와있지만 server1 코드를 굳이 다룰 것 없이 server2 코드를 다루면 충분할 것 같아서 server1은 패쓰 server2로 생성되는 서버는 /count 가 붙은 요청에 대해서는 count를 출력하는 핸들러를, 그 외 모든 요청에 대해서는 count를 증가시키는 핸들러를 호출한다.

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

var mu sync.Mutex
var count int

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler는 요청된 URL r의 Path 구성 요소를 반환한다.
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter는 지금까지 요청된 수를 반환한다.
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

main 함수 내에 두 개의 핸들러를 연결하는 코드가 있고 각 핸들러에서 하는 역할은 그 아래에 정의되어 있다. 이 경우 count변수를 여러 요청에서 동시에 접근하는 것을 막기 위해 count 변수를 증가시키거나 출력하는 부분 앞뒤로 lock을 걸어놓은 것을 볼 수 있다. 이 내용도 병렬 처리에서 중요한 내용이니 9장에 함께 나온다고 한다.

server3 코드는 HTTP request를 다시 출력해주는 역할을 한다는데 크게 설명할 내용이 없어 보여서 넘기려고 한다. 마지막에는 lissajous를 웹에 띄우는 기능을 보여주는데

package main

import (
    "log"
    "net/http"
    "image"
    "image/color"
    "image/gif"
    "io"
    "math"
    "math/rand"
)

const (
    whiteIndex = 0 // 팔레트의 첫 번째 색상
    blackIndex = 1 // 팔레트의 다음 색상
)

var palette = []color.Color{color.White, color.Black}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler는 HTTP 요청을 반환한다.
func handler(w http.ResponseWriter, r *http.Request) {
    lissajous(w)
}

func lissajous(out io.Writer) {
    const (
        cycles = 5      // x 진동자의 회전수
        res = 0.001     // 회전각
        size = 100      // 이미지 캔버스 크기 [-size..+size]
        nframes = 64    // 애니메이션 프레임 수
        delay = 8       // 10ms 단위의 프레임 간 지연
    )
    freq := rand.Float64() * 3.0 // y 진동자의 상대적 진동수
    anim := gif.GIF{LoopCount: nframes}
    phase := 0.0 // 위상 차이
    for i := 0; i < nframes; i++ {
        rect := image.Rect(0, 0, 2*size+1, 2*size+1)
        img := image.NewPaletted(rect, palette)
        for t := 0.0; t < cycles*2*math.Pi; t += res {
            x := math.Sin(t)
            y := math.Sin(t*freq + phase)
            img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex)
        }
        phase += 0.1
        anim.Delay = append(anim.Delay, delay)
        anim.Image = append(anim.Image, img)
    }
    gif.EncodeAll(out, &anim) // NOTE: 인코딩 오류 무시
}

이렇게 두 파일을 잘 합쳐주고 ResponseWriter에 쓰게끔 하면 localhost:8000으로 접속했을 때 우아한 리사주 곡선을 볼 수 있다. image

원래 Exercise를 다 무시했었는데 URL에서 parameter를 받아와 리사주 곡선을 바꾸라는 예제는 재미있어 보여서 복잡하지 않게 parameter로 r, g, b를 받아와서 바꾸는 코드를 작성해 보았다. 핸들러 내에서 request에 담겨있는 rgb값을 추출하고 이를 color로 만들어 lissajous 함수로 전달하고 lissajous 함수 내에서는 전달받은 값으로 line color를 재할당하는 코드를 구현했다. 코드를 다 적기는 좀 길어지니 바뀐 부분만 적어보자면(전체 코드는 ch1/lissajousserver.go 참고!)

func handler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query()
    red, _ := strconv.Atoi(query.Get("r"))
    green, _ := strconv.Atoi(query.Get("g"))
    blue, _ := strconv.Atoi(query.Get("b"))

    c := color.RGBA{uint8(red), uint8(green), uint8(blue), 255}
    lissajous(w, c)
}

handler 함수에서 r의 query 내용을 받아와 정수로 바꿔주는 부분이 추가되었고

func lissajous(out io.Writer, c color.Color) {
    palette[lineIndex] = c
...
}

lissajous 함수 가장 위에서 받아온 c를 lineIndex(기존 코드의 blackIndex 역할)에 할당했다. 그리고 결과는! image 😀

ytbeom commented 3 years ago

1.8. Loose Ends

튜토리얼에서 설명하지 못한 몇 가지 얘기들을 하는 것으로 보인다. 뒤에 제대로 나올 것들이니 크게 신경쓰지는 않지만 일단 적어보자면

어찌어찌 첫 단원을 끝냈다. 번역본으로 보다가 번역이 영 이상해 보여서 원서로 넘어왔더니 중간중간 설명이 급 부족해진 부분도 있는 것 같아서 슬프다... Exercise도 다 건너뛰면서 했는데, 막상 해보니 상당히 재미져서 앞으로 조금씩은 풀어볼 생각이다. 시작이 반이니 차근차근 읽다 보면 되겠지!