10110 การทำ Flag ด้วย Bitset ใน Golang

zapkub

Rungsikorn Rungsikavanich

Posted on December 5, 2020

10110 การทำ Flag ด้วย Bitset  ใน Golang

โปรแกรมเมอร์รุ่นใหม่ๆ ที่ไม่คุ้นกับภาษา Go (หรือภาษาเก่าๆอื่นๆ) อาจจะสงสัยว่า ไอ้ parameter ที่มัน OR ได้นี่มันคืออะหยังหนอ

    os.OpenFile("pathtofile", os.O_APPEND|os.O_RDWR, 0755)
                                      ^^^ ไอนี
    log.SetFlags(log.Llongfile | log.LUTC)
                         ^^^ ไอพวกนีวย
Enter fullscreen mode Exit fullscreen mode

มันคือ Flags นั่นเองครับ underhood มันคือ int ธรรมดาแต่ถูกจัดการด้วย bitwise operator ซึ่งจะเป็นหัวข้อของ blog นี้

🌟 TL;DR สรุปก่อนไปเลย

ตัวอย่างสุดท้าย ใน Playground
Bitset Flag จะทำให้เราสามารถ pass enum parameter ได้มากกว่า 1 parameter โดยไม่ต้องแก้ไข signature ของ function เพิ่ม ทำให้สามารถส่ง enum เข้าไปใน function ได้โดยไม่ต้องใช้ slice

การทำ Flag ใน Golang เหมือนกับการทำ Flag ในภาษาอื่นๆ แต่ด้วยความสามารถของ iota ทำให้การทำ Flag ง่ายขึ้นนิดนึง


type flag int

const (
    flagWithA flag = 1 << iota
    flagWithB
    flagWithC
    flagWithD
)

func (f flag) has(v flag) bool {
    return f&v != 0
}

func print(flags flag) {
    fmt.Println(
        flags.has(flagWithA),
        flags.has(flagWithB),
        flags.has(flagWithC),
        flags.has(flagWithD),
    )
}
func ExampleBitsetFlag() {
    print(flagWithA | flagWithD)
    // output: true false false true
}
Enter fullscreen mode Exit fullscreen mode

Flag คืออะไร

การพัฒนา application ด้วย Golang คงจะเคยเจอการใช้ Flag กันผ่านๆมาแล้ว โดยเฉพาะการ OpenFile

os.OpenFile("somedir", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0655)
                        ^^^^ ตรงนี
Enter fullscreen mode Exit fullscreen mode

ใน parameter ที่ 2 จะเห็นว่าเราสามารถ pass parameter ไปได้มากกว่า 1 ตัวทั้งๆที่ parameter ตรงนี้กำหนดไว้แค่ int อย่างเดียวใน signature

// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.). If the file does not exist, and the O_CREATE flag
// is passed, it is created with mode perm (before umask). If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
Enter fullscreen mode Exit fullscreen mode

หลายคนที่ย้ายมาจากภาษาอื่นๆ อาจจะไม่คุ้นตากับการใช้ Flag แบบนี้สักเท่าไหร่ (เนื่องจากภาษาอื่นก็มี pattern อื่นๆในการ pass value แบบนี้แทนแล้ว)

แต่เนื่องจาก Golang พัฒนามากับความเรียบง่าย เราเลยยังเห็นการใช้ Bitset และ bitwise operator เพื่อทำ flag parameter อยู่

Bitwise Operator คืออะไร?

ในภาษาต่างๆ เราอาจจะเคยเห็นการทำ Bitwise Operator กันมาบ้าง ถ้าเป็น Developer ใหม่ๆอาจจะไม่คุ้นชินตาเท่าไหร่ (ถ้าไม่ใช่ภาษาอย่าง C หรือ C++ หรือตอนเรียนมหาลัย)

ในบทความนี้มี 3 bitwise operators ที่เราจะใช้งานกันคือ

  • << left shift
  • & AND
  • | OR

Left shift operator

เครื่องหมาย << หมายถึงการขยับ bit value ไปด้านซ้าย (และกลับด้านกัน >> right shift คือขยับไปขวา)

package main
import "fmt"
func ExampleBitwise() {
    var n = 2 << 8
    fmt.Println(n)
    fmt.Println(n >> 2)
    // output: 512
    // 128
}

Enter fullscreen mode Exit fullscreen mode

ในตัวอย่างจะเห็นว่ามีการ assign left shift int(2) ให้กับตัวแปร n สิ่งที่ได้จาก expression นี้คือเราทำการขยับ bit ปัจจุบันไปด้านซ้าย จำนวน 8 ตัวให้กับ int(2)

Decimal Number Binary Number
2 10 ( มี 0 จำนวน 1 ตัว )
512 1000000000 (มี 0 จำนวน 9 ตัว )

AND และ OR operator

สำหรับการทำ AND operator เราจะได้ผลลัพธ์ออกมาเป็น bitset ที่ตรงกัน สำหรับ 2 values (intersect)

และ OR operator เราจะได้ผลลัพธ์ของ bit position ที่มีค่า (union)

ตัวอย่างเช่น

func ExampleBitsetAnd() {
    var n = 4   // this is 0100
    var nn = 12 // this is 1100
    fmt.Printf("%b %b", n&nn, n|nn)
    // output: 100 1100
}
Enter fullscreen mode Exit fullscreen mode

สร้าง Enum ด้วยค่าของ bit set

จาก Operator ที่ได้เกริ่นมา จะทำให้เราสามารถเอาหลักการ bitset มาใช้แทน value ของ flags ได้ด้วยการให้แต่ละ Bit บ่งบอกถึงการเปิดและปิดของ flag ต่างๆ

ยกตัวอย่างเช่น
Function echoHello สามารถ echo Hello ในภาษาต่างๆได้

       0000  // 4 bits
       ^ first bit represents echo in English
        ^ second bit represents echo in Thai
         ^ third bit represents echo in Japanese
          ^ last bit represents echo in French
Enter fullscreen mode Exit fullscreen mode

เราก็จะได้ enum ออกมาเป็นแบบนี้

const (
    EchoEN = 1 // 0001
    EchoTH = 2 // 0010
    EchoJP = 4 // 0100
    EchoFR = 8 // 1000
)

func ExampleEchoHello() {
    // ส่ง 1011 เข้าไป
    echoHello(EchoEN | EchoTH | EchoFR)
    // output: Hello
    // สวัสดี
    // Bonjour
}

func echoHello(flags int) {  // flags input คือ 1011
   if flags&EchoEN != 0 { // ผลลัพธ์ของการ 1011 AND 0001 คือ 0001
      fmt.Println("Hello")
   }
   if flags&EchoTH != 0 { // ผลลัพธ์ของการ 1011 AND 0010 คือ 0010
      fmt.Println("สวัสดี")
   }
   // .... do the rest of logic
}


Enter fullscreen mode Exit fullscreen mode

จากตัวอย่างชุดโปรแกรมด้านบน จะทำให้เราเห็นว่า เราสามารถใช้ bitset เพื่อแทน ค่าของ boolean หลายๆตัวได้ โดยไม่ต้องใช้ slice

ใช้ iota และ เปลี่ยนจาก int เป็น type ใหม่เพื่อให้ bitset ใช้งานง่ายขึ้น

จาก code ด้านบนจะเป็นการเขียนโปรแกรมตรงๆ ไม่ได้ใช้คุณสมบัติอะไรพิเศษจาก Golang ตอนนี้เราจะมาใช้คุณสมบัติของ Golang ให้ bitset flag เราใช้งานง่ายขึ้น

เริ่มจาก ประกาศ enum และ left shift โดยใช้ iota (คีย์เวิร์ดสำหรับทำ successive integer หรือเลขที่เพิ่มขึ้นเรื่อยๆ อธิบาย successive integer

const (
    EchoEN = 1 << iota // 1 << 0 = 0001
    EchoTH             // 1 << 1 = 0010
    EchoJP             // 1 << 2 = 0100
    EchoFR             // 1 << 3 = 1000
)
Enter fullscreen mode Exit fullscreen mode

จากนั้น เพื่อทำให้ง่ายต่อการเช็ค bit เราจะทำ type ใหม่สำหรับ Echo Flag และเขียน function เพื่อตรวจว่า bitset นั้นมี enum ที่เราอยากรู้มั้ย

type EchoFlag int
func (e EchoFlag) has(f EchoFlag) bool { 
     // ตัวอย่างถ้า e มีค่า = 12 ( 1100 )
     return e&f != 0 
     // ถ้า f คือ 0001 จะ return false เพราะ 1100 & 0001 = 0000
     // ถ้า f คือ 0100 จะ return true  เพราะ 1100 & 0100 = 0100
}
Enter fullscreen mode Exit fullscreen mode

จากนั้นเรามาแก้ให้ echoHello เรารับ EchoFlag และใช้ has() เพื่อเช็ค flag ของ input

func echoHello(flags EchoFlag) {
    if flags.has(EchoEN) {
        fmt.Println("Hello")
    }
    if flags.has(EchoTH) {
        fmt.Println("สวัสดี")
    }
    if flags.has(EchoJP) {
        fmt.Println("こんにちは")
    }
    if flags.has(EchoFR) {
        fmt.Println("Bonjour")
    }
}
Enter fullscreen mode Exit fullscreen mode

เสร็จหมดแล้ว! 🎊 🎉

ตัวอย่าง ใน Playground

แล้วมันเอาไว้ใช้ทำอะไร?

อย่างที่ยกตัวอย่างไปตั้งแต่แรก Native package ของ Golang อย่าง os จะใช้ Flag ในการทำ operation ต่างๆ โดยเฉพาะ arguments ที่จะส่งไปหาระบบปฏิบัติการ

os flag

หรือ package log ที่ใช้ flag ในการ set output option ของการ log
log flag

นอกจากนี้ผมก็ไม่รู้เหมือนกันว่าเอาไว้ทำอะไรอีก 🤨

จบสวัสดี 🙆‍♂️

💖 💪 🙅 🚩
zapkub
Rungsikorn Rungsikavanich

Posted on December 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related