avatar
thiti.dev
วงแหวนเว็บ
Search
Tags
My CV
Contact
Support me
Go EP.9 Go Context
27 Oct 2021

สวัสดีครับ ในบทความนี้ก็เป็น EP.9 แล้วนะครับ โดยเนื้อหาจะเป็นเรื่องเกี่ยวกับ Context ในภาษา Go ซึ่งเป็นเรื่องที่เราจะต้องได้เจอเมื่อเราพัฒนาโปรแกรมด้วยภาษา Go ครับ

สําหรับท่านใดที่ยังไม่ได้อ่าน EP.8 ท่านสามารถกลับไปอ่านก่อนได้นะครับที่นี่ Go EP.8 Go Channel Select Multiple Communication Operations

มาเริ่มเรียนรู้ไปด้วยกันตามหัวข้อด้านล่างเลยครับ

Go Context คืออะไร

Context ใน Go จะช่วยให้เราสามารถควบคุมและ จัดการ การทํางานของ Process ต่างๆ ในการทํางานแบบ Multitasking เช่น กําหนด timeout, กําหนด deadline หรือ การส่งข้อมูลผ่าน channel เพื่อ Share data ระหว่าง Process ต่างๆ

เพื่อให้เข้าใจมากขึ้นผมจะยกตัวอย่างที่ใช้กันบ่อยๆคือ กรณีที่เรา Request ไปเอาข้อมูลจาก API แล้วฝั่ง Server ทํางานช้าเกินกว่าที่เรากําหนด และเราไม่ต้องการที่จะรอ เราจึงต้องทําอะไรบางอย่างเพื่อที่จะหยุดการทํางานนั้นซะ เช่น การกําหนด Timeout เพื่อประสิทธิภาพโดยรวมของระบบ นี่แหละครับ Context จะมาช่วยเราในการจัดการเรื่องพวกนี้

การใช้งาน Go Context

ในการใช้งาน Go Context เราจะเริ่มด้วยการสร้าง Parent Context ด้วย function ต่างๆดังนี้ครับ

context.Background()

Function นี้จะ Return context ว่างๆ (Empty context) ออกมา โดยปกติจะใช้ใน Main function, initialization, test และ top-level Context ซึ่งเราสามารถสร้าง Context ใหม่ที่สืบทอดจาก Parent Context นี้ได้ (เดี๋ยวเราจะเรียนรู้กันในหัวข้อถัดไปครับ)

ctx := context.Background()

context.TODO()

Function นี้จะ Return context ว่างๆ (Empty context) ออกมา เหมือนกันกับ context.Background() ทุกประการ แต่ส่วนใหญ่ context.TODO() จะใช้ในกรณีที่เรายังไม่มีการใช้งาน Context แต่ Function มีการรับ Paramiter context ครับ ตัวอย่างการสร้างก็จะประมาณนี้ครับ

ctx := context.TODO()

สรุปคือทั้ง context.Background() และ context.TODO() คือการสร้าง Empty context ออกมาเหมือนกันครับ ใช้อันไหนก็ได้

Parent context ที่ได้จะมี Type เป็น Context interface โดยภายในจะประกอบไปด้วย Method ตามนี้ครับ

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error
    
    Value(key interface{}) interface{}
}

หลังจากที่เรารู้วิธีการสร้าง Parent Context แล้ว ขั้นตอนต่อไปเราจะไปเรียนรู้การการนํา Parent context ไปสร้างเป็น Context ใหม่ด้วยวิธีสืบทอดมาจาก Parent context ซึ่งการสร้าง Context ใหม่สามารถสร้างได้หลายแบบตามลักษณะของการนําไปใช้งานดังนี้

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

จะเห็นว่าในแต่ละแบบจะ Return new context ออกมา ซึ่ง Context ที่ Return ออกมานั้นจะถูก Implement method ต่างๆภายในออกมาด้วย แต่การ Implement method ก็จะแตกต่างกันไปในแต่ละ Function ครับ

มาดูรายละเอียดของแต่ละ Function ดังนี้ครับ

context.WithCancel(parent Context)

WithCancel จะ Return new context และ Function cancel ออกมาให้ โดยที่ New context ที่ได้จะถูก Implement method Done() ออกมาให้เราด้วย ซึ่งเราจะใช้ Done() ในการควบคุมการทํางานอีกทีครับ ลองดูตัวอย่างการใช้งานตามนี้ครับ

package main

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

func main() {
    messageCh := make(chan string)
    ctx, cancel := context.WithCancel(context.Background())
    
    go task(ctx, messageCh)
    
    select{
    case message := <-messageCh:
        fmt.Println(message)
    case <-time.After(2 * time.Second): // ลองเปลี่ยนเวลาตรงนี้ครับ ถ้าน้อยกว่าเวลาใน task จะทําให้ task ถูก Cancel
        cancel()
        fmt.Println(<-messageCh)
    }
}

func task(ctx context.Context, ch chan string) {
    select{
    case <- time.After(3 * time.Second):
        ch <- "Do task success."
    case <- ctx.Done():
        ch <- "Cancel task."
    }
}

// Result
// Cancel task.

จาก Code ด้านบน ผมสร้าง Function task สําหรับจําลองการทําอะไรสักอย่าง โดยกําหนด Delay time ไว้ และใน Function main จะมี Delay 2 วินาทีก่อนเรียก Function cancel() ซึ่ง Delay ใน Function main น้อยว่า Delay ใน Function task ทําให้ task ถูก Cancel ก่อนที่จะทํางานเสร็จ ผลของการรันจะได้เป็น "Cancel task."

แต่ถ้าเรากําหนดให้ Delay ใน Function main มากกว่า Delay ใน Function task จะได้ผลเป็นเป็น "Do task success." เนื่องจาก Delay น้อยกว่า ทํางานเสร็จก่อนที่จะ Cancel ครับ

WithDeadline(parent Context, deadline time.Time)

WithDeadline จะเหมือนกับ WithCancel เลยครับ แต่จะมีสิ่งที่เพื่มเติมเข้ามาคือ เราสามารถกําหนด Deadline ได้ หมายความว่า เมื่อถึงกําหนด Deadline แล้ว Done() channel จะถูก Close ทันที ลองดูตามตัวอย่างนี้ครับ

package main

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

const shortDuration = 1 * time.Second

func main() {
    d := time.Now().Add(shortDuration)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    defer cancel()

    select {
    case <-time.After(3 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }

}

// Result
// context deadline exceeded

จาก code ด้านบน เมื่อถึงกําหนด Deadline ก็จะทําให้ ctx.Done() close และก็แสดงผล ctx.Err() ออกมา นั้นก็คือ "context deadline exceeded" ครับ

context.WithTimeout(parent Context, timeout time.Duration)

WithTimeout ก็เหมือนกับ WithDeadline ทุกประการ แค่ต่างกันนิดนึงครับ ตรงที่ WithDeadline จะเป็นการกําหนด Deadline หรือ Expire time แต่ WithTimeout จะเป็นการกําหนด เป็น Timeout แทนครับ เพื่อให้เข้าใจมากขึ้น ลองดูตามตัวอย่างนี้ครับ

package main

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

const shortDuration = 1 * time.Millisecond

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }

}

// Result
// context deadline exceeded

จาก Code ด้านบนจะเห็นว่าการทํางานเหมือนกันกับ WithDeadline แต่จะต่างกันตรงที่ WithTimeout จะกําหนดเป็นระยะเวลา Timeout แทนครับ

context.WithValue(parent Context, key, val interface{})

สําหรับ WithValue จะต่างจาก Function อื่นๆ ครับ โดย WithValue จะไม่ return Function cancel มาให้ และการใช้งานจะเป็นการกําหนด Key และ Value เข้าไปใน Context ซึ่งส่วนใหญ่จะใช้ในการ Share ข้อมูลกันระหว่าง Function หรือ thread ต่างๆ

มาดูตัวอย่างกันครับ

package main

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

func main() {
    ctx := context.WithValue(context.Background(), "id", "A001")
    
    Get(ctx, "id")
    
    go func() {
        ctx2 := context.WithValue(ctx, "name", "Thiti")
        
        Get(ctx2, "id")
        
        go func() {
            Get(ctx2, "name")
        }()
    }()

    time.Sleep(1 * time.Second)
}

func Get(ctx context.Context, k string) {
    if v, ok := ctx.Value(k).(string); ok {
        fmt.Println(v)
    }
}

// Result:
// A001
// A001
// Thiti

จาก Code ด้านบนจะเห็นว่า ctx, ctx2 ถูกกําหนดค่า และใช้งานในหลายๆ Thread ครับ

ลองนําไปใช้งานกันดูนะครับ

ใน EP.10 จะเป็นเรื่องเกี่ยวกับ defer ในภาษา Go ท่านสามารถกดเข้าไปอ่านต่อกันได้ครับ

สําหรับบทความนี้ก็ขอจบไว้เพียงเท่านี้ครับ ขอบคุณครับ

thiti.dev © 2021 Thiti Yamsung