Published on

Golang Part 2: Advanced concepts in Go

Authors

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 แบบ คือ

  1. 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())
}
  1. 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 ที่ใช้เวลาทำงานนานๆ แบบนี้

main.go
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

main.go
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

main.go
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 ออกมาจากชื่อ

main.go
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 เพิ่มตรวจสอบแทน

main.go
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

main.go
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

main.go
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