10110 การทำ Flag ด้วย Bitset ใน Golang
Rungsikorn Rungsikavanich
Posted on December 5, 2020
โปรแกรมเมอร์รุ่นใหม่ๆ ที่ไม่คุ้นกับภาษา Go (หรือภาษาเก่าๆอื่นๆ) อาจจะสงสัยว่า ไอ้ parameter ที่มัน OR
ได้นี่มันคืออะหยังหนอ
os.OpenFile("pathtofile", os.O_APPEND|os.O_RDWR, 0755)
^^^ ไอ้นี่
log.SetFlags(log.Llongfile | log.LUTC)
^^^ ไอ้พวกนี้ด้วย
มันคือ 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
}
Flag คืออะไร
การพัฒนา application ด้วย Golang คงจะเคยเจอการใช้ Flag กันผ่านๆมาแล้ว โดยเฉพาะการ OpenFile
os.OpenFile("somedir", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0655)
^^^^ ตรงนี้
ใน 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) {
หลายคนที่ย้ายมาจากภาษาอื่นๆ อาจจะไม่คุ้นตากับการใช้ 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
}
ในตัวอย่างจะเห็นว่ามีการ 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
}
สร้าง 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
เราก็จะได้ 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
}
จากตัวอย่างชุดโปรแกรมด้านบน จะทำให้เราเห็นว่า เราสามารถใช้ 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
)
จากนั้น เพื่อทำให้ง่ายต่อการเช็ค 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
}
จากนั้นเรามาแก้ให้ 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")
}
}
เสร็จหมดแล้ว! 🎊 🎉
แล้วมันเอาไว้ใช้ทำอะไร?
อย่างที่ยกตัวอย่างไปตั้งแต่แรก Native package ของ Golang อย่าง os
จะใช้ Flag ในการทำ operation ต่างๆ โดยเฉพาะ arguments ที่จะส่งไปหาระบบปฏิบัติการ
หรือ package log
ที่ใช้ flag ในการ set output option ของการ log
นอกจากนี้ผมก็ไม่รู้เหมือนกันว่าเอาไว้ทำอะไรอีก 🤨
จบสวัสดี 🙆♂️
Posted on December 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.