- Published on
Golang Part 2: Advanced concepts in Go
- Authors
- Name
- Somprasong Damyos
- @somprasongd
Advanced concepts in Go
ในบทความนี้จะพูดถึงเรื่อง Advanced concepts ในภาษา Go ที่คิดว่าน่าจะต้องใช้บ่อยๆ หรือต้องเจอเมื่อมีการใช้งาน Library ต่างๆ
Advanced Functions
Variadic Function
คือ ฟังก์ชันที่รับค่าพารามิเตอร์เป็น (p ...T)
package main
import "fmt"
func main() {
y := sum(1, 2, 3, 4, 5)
fmt.Println(y)
}
func sum(x ...int) int {
s := 0
for _, v := range x {
s += v
}
return s
}
First Class Function
คือ ฟังก์ชันสามารถเป็นตัวแปร หรือเป็น field ใน struct
ก็ได้
// variable
var add = func(a, b int) int {
return a + b
}
fmt.Println(add(1, 2))
// field
m := Math{add: add}
fmt.Println(m.add(1, 2))
// ใน struct ประกาศแบบนี้
type Math struct {
add func(int, int) int
}
Higher Order Function
มี 2 แบบ คือ
- Function as Parameters คือ ฟังก์ชันใดๆที่รับพารามิเตอร์เป็นฟังก์ชันได้
package advfunc
import "fmt"
func LearnHOFParam() {
// ส่งไป anonymous function ไป
s := hofGreeting(func() string {
return "Ball"
})
fmt.Println(s)
}
// function ที่รับ parameter เป็น anonymous function ที่ return string
func hofGreeting(nameFn func() string) string {
return fmt.Sprintf("Hello %s", nameFn())
}
หรือจะใช้วิธีสร้างเป็น type ของ function ขึ้นมาแทนก็ได้ แบบนี้
package advfunc
import "fmt"
func LearnHOFParam() {
// ส่งไป anonymous function ไป
s := hofGreeting(func() string {
return "Ball"
})
fmt.Println(s)
}
// สร้าง type ใหม่ขึ้นมาเป็น function ที่ return string
type nameFunc func() string
// เปลี่ยนรับ parameter มาเป็น type ใหม่ที่สร้างมา
func hofGreeting(nameFn nameFunc) string {
return fmt.Sprintf("Hello %s", nameFn())
}
สามารถสร้าง function เป็นตัวแปร (First Class Function) แล้วส่งไปก็ได้
package advfunc
import "fmt"
func LearnHOFParam() {
// สร้างตัวแปรแบบ first class function
nameFn := func() string {
return "Ball"
}
// ส่งตัวแปรไปแทน
s := hofGreeting(nameFn)
fmt.Println(s)
}
// สร้าง type ใหม่ขึ้นมาเป็น function ที่ return string
type nameFunc func() string
// เปลี่ยนรับ parameter มาเป็น type ใหม่ที่สร้างมา
func hofGreeting(nameFn nameFunc) string {
return fmt.Sprintf("Hello %s", nameFn())
}
- Function as Return คือ ฟังก์ชันที่
return
เป็นฟังก์ชันได้
package advfunc
import "fmt"
func LearnHOFReturn() {
s := hofGreeting(newNameFunc("Ball"))
fmt.Println(s)
}
func newNameFunc(name string) nameFunc {
// สร้างตัวแปรแบบ first class function
nameFn := func() string {
return name // เปลี่ยนมาใช้ค่าจากที่ส่งมาแทน
}
return nameFn
}
// สร้าง type ใหม่ขึ้นมาเป็น function ที่ return string
type nameFunc func() string
// เปลี่ยนรับ parameter มาเป็น type ใหม่ที่สร้างมา
func hofGreeting(nameFn nameFunc) string {
return fmt.Sprintf("Hello %s", nameFn())
}
Clossure Function
คือ ฟังก์ชันที่ return ฟังก์ชัน โดยที่ฟังก์ชันที่ return ออกไป ต้องเรียกใช้ตัวแปรจากฟังก์ชันหลักด้วย
package advfunc
import "fmt"
func LearnClosure() {
counterFn := newCounterFunc()
fmt.Println(counterFn())
fmt.Println(counterFn())
fmt.Println(counterFn())
}
func newCounterFunc() func() int {
var i int
return func() int {
i++
return i
}
}
// output
// 1
// 2
// 3
Type Assertions
ในภาษา Go สามารถสร้าง interface เปล่าๆ ขึ้นมารับค่าอะไรก็ได้ แต่การที่จะเอา interface นั้นๆ มาเปรียบเทียบค่ากันตรงๆ กับข้อมูล type อื่นๆ จะทำตรงๆ ไม่ได้ เนื่องจากภาษา Go นั้นเป็น static type
ต้องใช้วิธีการทำ Assertions โดยใช้ .(T)
var i interface{}
i = "text"
var s string
// แบบนี้จะทำไม่ได้
s = i
// ต้องทำ assertion ใช้ .(type)
s = i.(string)
ปัญหาคือ ถ้าทำ assertion ผิด type จะเกิด panic ขึ้น ดังนั้นจะต้องตรวจสอบก่อน
var i interface{}
i = "text"
var s string
if v, ok := i.(string); ok {
s = v
}
กรณีที่เป็น type ได้หลาย type สามารถเอา switch case มาช่วยได้
func whichType(i interface{}) {
switch v := i.(type) {
case string:
fmt.Printf("this is a string %v\n", v)
case int:
fmt.Printf("this is a integer %v\n", v)
default:
fmt.Println("don't know type")
}
Defer
ในภาษา Go ถ้าใส่ defer
ไว้หน้า statement จะทำให้ statment นั้น จะเลื่อนการ execute ออกไปทำตอนฟังก์ชันนั้นๆ จะ return
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// output
hello
world
แต่ค่า arguments
นั่นจะถูกประมวลผลทันที ตัวอย่างเช่น
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 3; i++ {
defer fmt.Println(i)
// เท่ากับแบบนี้
// defer fmt.Println(0)
// defer fmt.Println(1)
// defer fmt.Println(2)
}
fmt.Println("done")
}
// output
// counting
// done
// 2
// 1
// 0
Goroutine
Goroutine คือ การทำให้โค้ดทำงานแบบ concurrent ในลักษณะของ lightweight thread โดย Go จะไปสร้าง layer ออกมาอีกชั้นหนึ่ง ซึ่งไม่ใช่ thread จริงๆ จึงใช้ memory น้อยกว่า
การใช้งาน Goroutine แค่ใส่ keyword ว่า go
หน้าฟังก์ชันก็จะได้ goroutine เลย
ตัวอย่าง ถ้าต้องมี function ที่ใช้เวลาทำงานนานๆ แบบนี้
package goroutine
import (
"fmt"
"time"
)
func LearnGR() {
start := time.Now()
doLongTask(1)
doLongTask(2)
doLongTask(3)
time.Sleep(120 * time.Millisecond)
fmt.Printf("Done in %v\n", time.Since(start))
}
func doLongTask(taskId int) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("task id: %d done.\n", taskId)
}
// Output
// task id: 1 done.
// task id: 2 done.
// task id: 3 done.
// Done in 425.555828ms
เอา goroutine มาช่วย
package goroutine
import (
"fmt"
"time"
)
func LearnGR() {
start := time.Now()
go doLongTask(1)
go doLongTask(2)
go doLongTask(3)
time.Sleep(120 * time.Millisecond)
fmt.Printf("Done in %v\n", time.Since(start))
}
func doLongTask(taskId int) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("task id: %d done.\n", taskId)
}
// Output
// task id: 3 done.
// task id: 2 done.
// task id: 1 done.
// Done in 120.666304ms
Waitgroup
การใช้งาน Goroutine ถ้าหาก main thread จบการทำงานไปก่อน จะทำให้ goroutine อื่นๆ จะจบการทำงานไปด้วย
จากตัวอย่างด้านบน จะเห็นว่ามีการใส่ time.Sleep(120 * time.Millisecond)
เอาไว้เพื่อรอให้ Goroutine ทำงานจบก่อน ซึ่งเป็นการคาดการเวลาเผื่อเอาไว้
ถ้าต้องการรอให้ goroutine ทั้งหมด ทำงานจบแล้วจริงๆ ต้องใช้ sync.WaitGroup
เข้ามาช่วยแทน
package goroutine
import (
"fmt"
"sync"
"time"
)
func LearnWG() {
start := time.Now()
wg := &sync.WaitGroup{}
// ระบุจำนวน
wg.Add(3)
go doLongTaskWG(1, wg)
go doLongTaskWG(2, wg)
go doLongTaskWG(3, wg)
// หยุดคอยจนกว่าจะครบตามจำนวนที่ระบุ
wg.Wait()
fmt.Printf("Done in %v\n", time.Since(start))
}
func doLongTaskWG(taskId int, wg *sync.WaitGroup) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("task id: %d done.\n", taskId)
// เมื่อทำงานเสร็จต้องว่า done
wg.Done()
}
Race Condition
Race Condition คือ การที่ goroutine มากกว่า 1 ตัว พยายามเข้าถึงตัวแปรตัวเดียวในเวลาเดียวกัน สามารถป้องกันได้ โดยการใช้ mux.Lock()
และเมื่อใช้งานเสร็จก็สั่ง mux.Unlock()
package goroutine
import (
"fmt"
"sync"
)
var i int
var mux sync.Mutex
func LearnRC() {
go func() {
for {
fmt.Println(read())
}
}()
for i := 0; i < 10; i++ {
write(i)
}
}
func write(n int) {
mux.Lock()
i = n
mux.Unlock()
}
func read() int {
mux.Lock()
// unlock หลัง return
defer mux.Unlock()
return i
}
Channel
Channel คือ ช่องทางที่ goroutine เอาไว้คุยกันทั้งการส่ง และรับค่าระหว่าง goroutine ด้วยกัน หรือ goroutine กับ main thread
Channel มี 2 แบบ
- Unbuffered เป็น guaranteed synchronization คือ ถ้าฝั่งส่ง ส่งไปแล้วไม่มีใครมารับ ของที่ส่งจะยังคาอยู่ที่ฝั่งส่ง สร้างโดยใช้
make(chan string)
- Buffered สร้างโดยใช้
make(chan string, 5)
ซึ่ง 5 คือ จำนวน buffered ดังนั้น ฝั่งส่งสามารถส่งข้อมูลไปได้เลย ถึงแม้จะยังไม่มีใครมารับค่า แต่จะส่งมาได้เท่ากับจำนวน buffered ซึ่งถ้าเต็มจะทำให้ส่งมาไม่ได้ ต้องรอจนว่าจะมีใครมารับค่าไปก่อน
การรับส่งก็มี 2 แบบ
- 2-way Channel การประกาศแบบปกติจะสามารถอ่าน และเขียนได้
- 1-way Channel ระบุว่าเป็นอ่านอย่างเดียว หรือเขียนอย่างเดียว
ตัวอย่างการใช้งาน Channel
package goroutine
import "fmt"
func LearnChan() {
ch := make(chan int)
go fibo(ch)
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}
func fibo(ch chan int) {
a, b := 0, 1
for{
ch <- a
a, b = b, a+b
}
}
Select Statement
เมื่อมีการสั่งให้รอรับค่าจาก channel จะทำให้โค้ดโดน block ที่จุดนั้นทันที แล้วถ้าเรามีมากกว่า 1 channel แล้วต้องการเลือกว่าจะทำงานต่อเมื่อมีค่าจาก channel ใด channel หนึ่งส่งมา แล้วให้ทำงานเลย จะต้องใช้ select statement มาช่วย
package goroutine
import "fmt"
func LearnChanWithSelect() {
ch := make(chan int)
qCh := make(chan struct{})
go fibo2(ch, qCh)
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
// ส่งไปบอกว่าให้จบการทำงาน
qCh <- struct{}{}
// รอรับการตอบกลับ
<-qCh
fmt.Println("Done")
}
func fibo2(ch chan int, qCh chan struct{}) {
a, b := 0, 1
for{
select{
case ch <- a:
a, b = b, a+b
case <-qCh:
// ตอบกลับว่าได้รับสัญญาณแล้ว
qCh <- struct{}{}
return
}
}
}
Work with JSON
ในภาษา Go นั้นมี standard library ชื่อ encoding/json เตรียมไว้ให้แล้วในการแปลงค่าจะ type ต่างๆ ไปเป็น JSON เรียกว่า Marshal ซึ่งส่วนใหญ่เราจะใช้งานในการทำ API โดยใช้แปลง struct ให้เป็น JSON เพื่อส่งกลับไป และสามารถแปลงจากข้อความที่อยู่ในรูปแบบของ JSON ไปเป็น map หรือ struct เรีกยว่าการ Unmarshal
JSON Marshal map
package main
import (
"encoding/json"
"fmt"
"time"
)
func main() {
m := map[string]interface{}{
"Name": "Ball",
"Age": 37,
"Active": true,
"lastLoginAt": time.Now(),
}
u, err := json.Marshal(m)
if err != nil {
panic(err)
}
fmt.Println(string(u)) // {"Name":"Ball","Age":37,"Active":true,"lastLoginAt":"2022-01-08T09:50:03.4819564+07:00"}
}
JSON Marshal Struct
package main
import (
"encoding/json"
"fmt"
"time"
)
type User struct {
Name string
Age int
Active bool
lastLoginAt time.Time
}
func main() {
u, err := json.Marshal(User{Name: "Ball", Age: 35, Active: true, lastLoginAt: time.Now()})
if err != nil {
panic(err)
}
fmt.Println(string(u)) // {"Name":"Ball","Age":35,"Active":true}
}
JSON Marshal Struct with JSON Tags
ถ้าต้องการแก้ไขชื่อ field ให้เป็นชื่ออื่น หรือไม่ต้องการให้แสดง field ออกมา ทำได้โดยการใช้ JSON tags json:""
package main
import (
"encoding/json"
"fmt"
"time"
)
type User struct {
Name string `json:"full_name"`
Age int `json:"age,omitempty"`
Active bool `json:"-"`
lastLoginAt time.Time
}
func main() {
u, err := json.Marshal(User{Name: "Ball", Age: 35, Active: true, lastLoginAt: time.Now()})
if err != nil {
panic(err)
}
fmt.Println(string(u)) // {"full_name":"Ball","age":35}
u, err = json.Marshal(User{Name: "Somprasong Damyos", Age: 0, Active: true, lastLoginAt: time.Now()})
if err != nil {
panic(err)
}
fmt.Println(string(u)) // {"full_name":"Somprasong Damyos"}
}
JSON Unmarshal to map
func main() {
var m map[string]interface{}
str := `{"Active":true,"Age":37,"Name":"Ball","lastLoginAt":"2022-10-14T09:50:03.4819564+07:00"}`
b := []byte(str)
if err := json.Unmarshal(b, &m); err != nil {
panic(err)
}
fmt.Printf("%#v", m)
// {"Active":true, "Age":37, "Name":"Ball", "lastLoginAt":"2022-10-14T09:50:03.4819564+07:00"}
}
JSON Unmarshal to struct
func main() {
u := User{}
str := `{"Active":true,"Age":37,"Name":"Ball","lastLoginAt":"2022-10-14T09:50:03.4819564+07:00"}`
b := []byte(str)
if err := json.Unmarshal(b, &u); err != nil {
panic(err)
}
fmt.Printf("%#v\n", u)
fmt.Println(u.lastLoginAt)
}
// Output
main.User{Name:"", Age:37, Active:false, lastLoginAt:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}
0001-01-01 00:00:00 +0000 UTC
Go environment variables
ในการพัฒนาเราสามารถกำหนดค่า configuration ได้ตาม process ที่ใช้รัน เช่นตอน dev ก็ใช้ค่าหนึ่ง ส่วนตอนใช้งานจริงก็ใช้อีกค่าหนึ่ง ซึ่งการจัดการ environment variables ในภาษา Go ทำได้ดังนี้
Go os.Getenv
Getenv
ใช้ในการอ่านค่า environment variable ออกมาจากชื่อ
package main
import (
"fmt"
"os"
)
func main() {
key := "PORT"
fmt.Println("Port:", os.Getenv(key)) // "3000" or "" ถ้าไม่มี key
}
Go os.LookupEnv
ปัญหาของการใช้ Getenv
คือ ถ้าไม่มี key ที่ระบุไปจะได้ค่าว่างออกมา ซึ่งทำให้เราไม่รู้ว่ามันมี key นั้นจริงแต่มีค่าว่าง หรือไม่มี key กันแน่ ดังนั้นให้ใช้ LookupEnv
เพิ่มตรวจสอบแทน
package main
import (
"fmt"
"os"
)
func main() {
key := "PORT"
val, ok := os.LookupEnv(key)
if !ok {
fmt.Printf("%s not set\n", key)
} else {
fmt.Printf("%s=%s\n", key, val)
}
fmt.Println(GetEnvInt(key, 3000))
}
// หรือจะสร้างเป็นตัวช่วยถ้าไม่มี key ให้คืนค่า default value มาแทนแบบนี้
func GetEnv(key string, defaultVal string) string {
val, ok := os.LookupEnv(key)
if !ok {
return defaultVal
}
return val
}
func GetEnvInt(key string, defaultVal int) int {
val, ok := os.LookupEnv(key)
if !ok {
return defaultVal
}
v, err := strconv.Atoi(val)
if err != nil {
return defaultVal
}
return v
}
func GetEnvBool(key string, defaultVal bool) bool {
val, ok := os.LookupEnv(key)
if !ok {
return defaultVal
}
v, err := strconv.ParseBool(val)
if err != nil {
return defaultVal
}
return v
}
Go os.Setenv
้ใช้ Setenv
ในการกำหนดค่า environment variable
package main
import (
"fmt"
"os"
)
func main() {
key := "PORT"
fmt.Println("Port:", os.Getenv(key))
os.Setenv(key, "3000")
fmt.Println("Port:", os.Getenv(key))
}
// Out put
Port:
Port: 3000
Go list environment variables
เมื่อใช้ Environ
จะค่า []string ออกมา โดย string แต่ละตัวจะมีรูปแบบ key=value
package main
import (
"fmt"
"os"
"strings"
)
func main() {
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
fmt.Printf("%s: %s\n", pair[0], pair[1])
}
}
ก็จบแล้วสำหรับเนื้อหา Advanced concepts ของภาษา Go ซึ่งแน่นอนว่ายังมีอีกหลายๆ เรื่องที่ไม่กล่าวถึง แต่คิดว่าน่าจะเพียงพอต่อการนำเอาภาษา Go ไปสร้าง API Service แล้ว
บทความถัดไปเป็นบทความปิดท้ายของบทความชุดนี้แล้ว ซึ่งเมื่อเราเขียนโค้ดขึ้นมาแล้วเราต้องทดสอบ มาดูกันว่า Go มีวิธีการเขียน Test