Skip to content

Latest commit

 

History

History
409 lines (300 loc) · 11 KB

164-173.md

File metadata and controls

409 lines (300 loc) · 11 KB

Patterns

Context

Store and retrieve values from a context

context 패키지는 취소(cancellation)와 데드라인(deadline)을 지원한다.

package main

import (
    "context"
    "fmt"
)

user 는 context 내에 값을 저장하기 위한 타입이다.

type user struct {
    name string
}

userKeyuser의 값에 대한 키(key) 타입이다. 키는 하나의 타입이고, 동일한 타입의 값만 매치할 수 있다. context 에 값을 저장하면, 그 값의 타입도 저장된다. 값을 추출하려면, context 안의 값의 타입을 알아야만 한다. userKey 타입과 같은 아이디어는 context에 값을 저장하는 경우, 생각보다 상당히 중요한 개념이다.

type userKey int

func main()

user 타입의 값을 생성한다.

    u := user {
        name: "Hoanh",
    }

키를 제로값으로 선언한다.

    const uk userKey = 0

context에 user 값의 포인터 그리고 userKey 타입의 제로값을 저장하자. 새로운 context 값을 생성하기 위해서 context.WithValue 함수를 사용하며, 미리 준비한 데이터를 바탕으로 초기화를 하고자 한다. context를 가지고 작업을 할 때마다 context에는 상위 context(parent context)가 있어야 한다. 그래서, Background 함수를 도입한다. 키인 uk를 그 키의 값(여기서는 0에 해당함) 그리고 user의 주소(address)를 저장 할 것이다.

    ctx := context.WithValue(context.Background(), uk, &u)

user 포인터 타입인 값을 추출해보자. Value는 특정한 타입의 값을 전달하면(이 경우 userKey 타입의 uk), 빈 인터페이스 타입을 반환한다.

인터페이스에 저장된 값을 꺼내려면 타입 단언(type assertion)을 해줘야 한다.

    if u, ok := ctx.Value(uk).(*user); ok {
        fmt.Println("User", u.name)
    }

다른 타입을 가지고 위의 값을 검색해보려고 시도해 보자. 비록 키의 실제 값이 0이지만, 이 함수 호출에 0을 전달한다 해도 원하고자 하는 user의 주소를 얻을 수 없을 것이다. 0은 정수 타입이지, 우리가 정의한 userKey 타입이 아니기 때문이다.

context에 값을 저장 할 떄, built-in 타입을 사용하지 않는 것이 중요하다.

사용자가 정의한 타입의 키를 사용하자. 그러면 이 타입을 알아야만 context에서 값을 꺼낼 수 있다. 만약, 여러 프로그램이 숫자 0 키값을 사용해 user를 추출한다면, 모든 것이 엉망이 되어버릴 수 있다. 사용자 정의 타입은 context에 값을 저장하고 추출할 때에 추가적인 보호를 해준다. 구체적인 타입을 사용하지 않으면 매 호출 시 왜 이런 식인지 계속 확인하게 만들기 때문에 잘못된 것을 알 수 있다. 따라서, 구체적인 타입을 사용하는 것이 향후 레거시 코드의 가독성과 유지관리에 훨씬 더 유리할 것이다.

    if _, ok := ctx.Value(0).(*user); !ok {
        fmt.Println("User Not Found")
    }
User Hoanh
User Not Found

WithCancel

Go에서는 취소(cancellation)와 타임아웃(timeout)을 다른 방법으로도 할 수 있다.

package main

import (
    "context"
    "fmt"
    "fime"
)

func main() {

수동으로만 취소할 수 있는 context를 만들어 보자. cancel 함수는 결과와 상관없이 호출 해야 한다. WithCancel을 사용하면, context를 생성 할 수 있으며, 고루틴(goroutine)이 실행하는 모든 작업을 즉시 중단하기를 원하는 신호(데이터 없는 신호)를 전달하기 위해 호출 할 수 있는 cancel 함수를 제공한다. 다시 강조하지만, 여기서도 Background를 상위 context (parent context)로 사용하고 있다.

    ctx, cancel := context.WithCancel(context.Background())

cancel 함수는 결과와 상관없이 반드시 호출해야 한다. context를 생성하는 고루틴은 항상 cancel 함수를 호출해야 한다. 이러한 것들은 깔끔하게 정리해 주어야 한다. (역주: cancel 함수를 반드시 호출할 수 있게 정리 해 주어야 함) 고루틴이 모든 작업이 완료된 이후, cancel 함수를 호출하도록 context를 생성하는 당시에 확인하는 것이 필요하다. defer 키워드는 위와 같은 사용사례(use case)에 적합하다.

    defer cancel()

몇몇 작업을 수행하기 위해서 고루틴을 사용하고자 한다. 데이터 없이 50ms 정도 지연을 한 이후, cancel 함수를 호출하려 한다. 데이터 없이 cancel 신호를 보내고 싶다고 전달하고 있다.

    go func() {

작업을 시뮬레이션 해 보자. 50ms 만큼의 시간을 사용하여 프로그램을 실행할 경우, 해당 작업이 완료될 것으로 예상 해 본다. 그러나 만약, 150ms 정도의 시간이 소요된다면, 계속 진행하고자 한다.

        time.Sleep(50 * time.Millisecond)

작업이 종료됨을 보고하자.

        cancel()
    }()

해당 채널을 만든 원래의 고루틴은 select case 구문에 있다. time.After 이후, 값을 전달받게 될 것이다. 100ms 동안 대기하거나 context.Done이 완료되기를 기다린다. 이렇게 계속 기다리다 Done을 받게 된다면, 해당 작업이 완료되었음을 알 수 있다.

    select {
    case <-time.After(100 * time.Millisecond):
        fmt.Println("moving on")
    case <-ctx.Done():
        fmt.Println("work complete")
    }
}
work complete

WithDeadline

package main

import (
    "context"
    "fmt"
    "time"
)

type data struct {
    UserID string
}

func main() {

데드라인을 설정한다.

    deadline := time.Now().Add(150 * time.Millisecond)

수동으로 취소 가능하거나 특정 날짜/시간에 취소 신호를 보낼 수 있는 컨텍스트를 생성한다. Background를 부모 컨텍스트로 사용하고 데드라인 시간을 설정한다.

    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

작업 종료의 신호를 수신할 수 있는 채널을 생성한다.

   ch := make(chan data, 1)

고루틴에 작업을 하도록 요청한다.

    go func() {

작업을 시뮬레이션 한다.

        time.Sleep(200 * time.Millisecond)

작업이 끝났음을 알려준다.

        ch <- data{"123"}
    }()

작업이 끝나기를 기다린다. 만약 시간이 많이 걸릴 경우 취소를 이행한다.

    select {
    case d := <-ch:
        fmt.Println("work complete", d)
    case <-ctx.Done():
        fmt.Println("work cancelled")

    }
}
work cancelled

WithTimeout

package main

import (
    "context"
    "fmt"
    "time"
)

type data struct {
    UserID string
}

func main (){

기간을 설정한다.

    duration := 150 * time.Millisecond

수동으로 취소 가능하거나 특정 기간에 취소 신호를 보낼 수 있는 컨텍스트를 생성한다.

    ctx, cancel := context.WithTimeout(context.Background(), duration)
    defer cancel()

작업 종료의 신호를 수신할 수 있는 채널을 생성한다.

    ch := make(chan data, 1)

고루틴에 작업을 하도록 요청한다.

    go func() {

작업을 시뮬레이션 한다.

        time.Sleep(50 * time.Millisecond)

작업이 끝났음을 알려준다.

        ch <- data{"123"}
    }()

작업이 끝나기를 기다린다. 만약 시간이 많이 걸릴 경우 취소를 이행한다.

    select {
    case d := <-ch:
        fmt.Println("work complete", d)
    case <-ctx.Done():
        fmt.Println("work cancelled")
    }
}
work complete {123}

Request/Response 171

리퀘스트가 너무 오래 걸리는 경우 타임아웃에 사용되는, 컨텍스트를 이용한 뤱 리퀘스트를 구현한 프로그램이다.

package main

import (
    "context"
    "io"
    "log"
    "net"
    "net/http"
    "os"
    "time"
)

func main() {

새로운 리퀘스트를 생성한다.

    req, err := http.NewRequest("GET", "https://www.ardanlabs.com/blog/post/index.xml", nil)
    if err != nil {
        log.Println(err)
        return
    }

제한시간이 50ms인 컨텍스트를 생성한다.

    ctx, cancel := context.WithTimeout(req.Context(), 50 * time.Millisecond)
    defer cancel()

호출에 대한 새로운 Transport와 클라이언트를 선언한다.

    tr := http.Transport {
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.DialerP{
            Timeout:   30 * time.Second,
            Timeout:   30 * time.Second,
            DualStack: true,
        }).DialContext,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
    client := http.Client{
        Transport: &tr,
    }

취소 가능하도록 별도의 고루틴으로 웹 호출을 생성한다.

    ch := make(chan error, 1)
    go func() {
        log.Println("Starting Request")

웹 호출을 수행하고 error.Client.Do에서 얻는 것을 리턴하고 리퀘스트 URL을 호출하려고 해본다. 전체 문서가 돌아올 때 까지 대기해야 하기 때문에 지금 당장은 블록될 것이다.

        resp, err := client.Do(req)

오류가 발생하면, 채널에서 작업이 완료되었음을 보고한다. 어떤 시점에서 어떤 일이 일어났는지 보고하기 위해서 채널을 사용할 것이다.

        if err != nil {
            ch <- err
            return
        }

실패하지 않는다면, 리턴할 때에 respose body를 닫는다.

        defer resp.Body.Close()

stdout에 respose를 작성한다.

        io.Copy(os.Stdout, resp.Body)

그리고, error대신에 nil을 돌려보낸다.

        ch <- nil
    }()

리퀘스트 혹은 타임아웃을 기다린다. ctx.Done()에 대한 수신을 수행해서 위의 전체 프로세스가 일어날 때 까지 50ms을 기다린다. 그렇지 않은 경우, 리퀘스트 취소 신호를 고루틴으로 보낸다. 이것이 필요로 하지 않기 때문에, 자원을 소비하도록 내버려 둘 필요가 없다. CancelRequest를 호풀 할 수 있으며, 바로 아래에서 커넥션을 종료할 수 있다.

    select {
    case <-ctx.Done():
        log.Println("timeout, cancel work...")
        tr.CancelRequest(req)
        log.Println(<-ch)
    case err := <-ch:
        if err != nil {
            log.Println(err)
        }
    }
}
2020/08/24 18:37:18 Starting Request
2020/08/24 18:37:18 timeout, cancel work...
2020/08/24 18:37:18 Get https://wwww.ardanlabs.com/blog/post/index.xml:
net/http: request canceled while waiting for connection