iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
0
自我挑戰組

Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說系列 第 20

#20 單向通道、位元運算子、const 實現枚舉 小狼小可交換身體啦!| Golang魔法使

  • 分享至 

  • xImage
  •  

先前因為篇輻關係有一些細節沒有補充到,所以第 20 天來補充細節吧!


這天因為小狼和小可一起抓住了替Golang牌,導致交換身體。我知道你們在想什麼,你們是不是在想怎麼不是小狼和小櫻換,幹!就是有你們這種人


單向通道

channel 通常都是可發送可接收,但如果今天我設計了一款套件,該套件會在某些情況透過 channel 發送訊息。如果今天使用者反過來透來 channel 傳送訊息,會導致套件出錯。那麼我要怎麼預防呢?

其實這個就是 time.After()

昨天的範圍有提到的函式,該函式在設定一段時間後,會透過通道傳回 Time 類型的 channel

func After(d Duration) <-chan Time

比較細心的魔法使應該有發現,回傳型態的左方多了 <- 。這個代表說該型態僅允許「接收」

package main
import "fmt"

func counter(start int, end int) <-chan int{
    channel := make(chan int)
    single := make(<-chan int)
    single = channel
    go func(){
        for start  <= end{
            channel <- start
            start = start + 1
        }
        close(channel)
    }()
    return single
}

func main(){
    // counter 回傳的 channel 僅用於接收
    // 若嘗試寫入將會出錯
    for k := range counter(0, 10){
        fmt.Println(k)
    }
}

執行結果:
0
1
2
3
4
5
6
7
8
9
10

如果嘗試寫入則會在編譯時出錯:

package main
import "fmt"

func counter(start int, end int) <-chan int{
    channel := make(chan int)
    single := make(<-chan int)
    single = channel
    go func(){
        for start  <= end{
            channel <- start
            start = start + 1
        }
        close(channel)
    }()
    return single
}

func main(){
    ch := counter(0, 10)
    for k := range ch{
        fmt.Println(k)
        // 嘗試寫入回去
        ch <- k
    }
}

# command-line-arguments
./lesson20.go:23:12: invalid operation: ch <- k (send to receive-only type <-chan int)

只能寫和只能讀的通道其道理相同,在此就先不示範了

僅能發送 make(chan type <-)
僅能接收 make(<- chan type)

運算子

在 golang 中有許多耳熟能詳的運算子,比如: + - * / % = 然而,這些賦值運算先前只草草帶過。所以今天要講細一點

賦值語法糖

package main
import "fmt"

func main(){
    var num int = 10
    num += 10 // 意思同 num = num + 10
    fmt.Println(num)
}

這個對初學者可能需要一些時間不習慣

num += 10 // num = num + 10
num -= 10 // num = num - 10
num *= 10 // num = num * 10
num /= 10 // num = num / 10
num %= 10 // num = num % 10

因為之前沒講所以帶範例時我都不敢用,也許之後會把這部分調到先前的範圍

遞增遞減語法糖

package main
import "fmt"

func main(){
    var num int = 10
    num++    // 同 num = num + 1
    fmt.Println(num)
    num--    // 同 num = num - 1
    fmt.Println(num)
}

執行結果:
11
10

可以很明顯的知道這裡指的 ++ 就是指將 num 遞增,而 -- 就是將 num 遞減。

然而,如果是學過其他語言的魔法使就會問那 ++num 呢?不好意思,被廢掉了。還有那 fmt.Println(num++) 呢?不好意思,也不支援,所以也沒有先加後加的問題了。
也因此遞增遞減運算符在 Golang 中變的比較沒那麼複雜

位元運算

位元運算指的是針對二進位的數所設計的運算元,常見的有 & (and), | (or), ^ (xor), &^ (bit clear), >> (right shift), << (left shift)

以下示範,用 0b 開頭來表示二進位,比如 0b10 代表 2, 0b100 代表 4

and, or, xor, bit clear

package main
import "fmt"

func main(){
    // 只有左右 bit 都為 1 ,結果才會是 1
    fmt.Printf("%b\n", 0b0 & 0b0)    // 0
    fmt.Printf("%b\n", 0b0 & 0b1)    // 0
    fmt.Printf("%b\n", 0b1 & 0b0)    // 0
    fmt.Printf("%b\n", 0b1 & 0b1)    // 1
    
    // 只要左右有一個 bit 為 1 結果的 bit 就是 1
    fmt.Printf("%b\n", 0b0 | 0b0)    // 0
    fmt.Printf("%b\n", 0b0 | 0b1)    // 1
    fmt.Printf("%b\n", 0b1 | 0b0)    // 1
    fmt.Printf("%b\n", 0b1 | 0b1)    // 1
    
    // 左右 bit 只能有一個 1 ,不然結果的 bit 會是 0
    fmt.Printf("%b\n", 0b0 ^ 0b0)    // 0
    fmt.Printf("%b\n", 0b0 ^ 0b1)    // 1
    fmt.Printf("%b\n", 0b1 ^ 0b0)    // 1
    fmt.Printf("%b\n", 0b1 ^ 0b1)    // 0

    // 位元清除是一個比較特殊的運算
    // 他會先將右式取 not 再合起來
    fmt.Printf("%b\n", 0b0 &^ 0b0)    // 0 & 1 -> 0
    fmt.Printf("%b\n", 0b0 &^ 0b1)    // 0 & 0 -> 0
    fmt.Printf("%b\n", 0b1 &^ 0b0)    // 1 & 1 -> 1
    fmt.Printf("%b\n", 0b1 &^ 0b1)    // 1 & 0 -> 0
}

這裡只拿一個位元舉例,但實際上在用時會搭配不只一個位元,初學者可以自主練習一下

package main
import "fmt"

func main(){
    fmt.Printf("%08b\n", 0b00001111 & 0b00110011)   // 00000011
    fmt.Printf("%08b\n", 0b00001111 | 0b00110011)   // 00111111
    fmt.Printf("%08b\n", 0b00001111 ^ 0b00110011)   // 00111100
    fmt.Printf("%08b\n", 0b00001111 &^ 0b00110011)  // 00001100
}

左移、右移

左移、右移這個就有趣了,在十進位中,如果我把數字左移一位,比如 15 左移一位變 150 左移兩位變 1500,也就是每左移一位數字會變 10 倍,反之每右移一位數字會變原先的十分之一。利用這個原理套用在 2 進位上,則數字每左移 1 位就會變 2 倍,左移兩位就會變 4 倍。

package main
import "fmt"

func main(){
    fmt.Printf("%04b\n", 0b0111 << 1)   // 1110
    fmt.Printf("%d\n", 15 << 1)         // 30

    fmt.Printf("%04b\n", 0b0111 >> 1)   // 0011
    fmt.Printf("%d\n", 15 >> 1)         // 7
}

const

如果你以為 golang 的 const 只能當常數那你就太小看 const 了。

package main
import "fmt"

func main(){
    const(
        C0 = iota
        C1 = iota
        C2 = iota
    )
    fmt.Println(C0)
    fmt.Println(C1)
    fmt.Println(C2)
}

執行結果:
0
1
2

利用 iota 可以自 0 開始產生連續數字。但是,如果只有這樣我自己打 0, 1, 2, ... 不就好了嗎?所以你可以更簡單的寫成:

package main
import "fmt"

func main(){
    const(
        C0 = iota
        C1
        C2
    )
    fmt.Println(C0)
    fmt.Println(C1)
    fmt.Println(C2)
}

執行結果:
0
1
2

如此一來就可以很輕鬆的實現 enum 枚舉 (許多魔法都有自帶 enum) 。但是,你以為這樣就結束了嗎?不!除了可以 1 次加 1 外,還能用來實現 flag 的功能

有時候我們在製作函式時會希望可以讓使用者條整一些參數,但如果使用 boolean 來調整其實很浪費,因為 boolean 雖然能用來表示 true 和 false 但實際上確是用了 8 個 bits 在表示,也就是 0000000false, 00000001true,其他 7 個 bit 根本沒用到。如果你想設計一個函式讓使用者可以分別調整 8 個 bit 的值,那麼你只需要花 8 個 bit 的空間,而不是 8 個 bytes (64bits)。足足省了 8 倍。

那麼要怎麼設 flag 呢?

你可以想像,比如模式 1 用 00000001,模式 2 用 00000010 如果 flag 是 00000011 就代表 模式1, 模式2 同時啟用。

package main
import "fmt"

const(
    mode1 = 0b0001
    mode2 = 0b0010
    mode3 = 0b0100
    mode4 = 0b1000
)

func render(flag int){
    if((flag & mode1) == mode1){
        fmt.Println("mode 1")
    }

    if((flag & mode2) == mode2){
        fmt.Println("mode 2")
    }

    if((flag & mode3) == mode3){
        fmt.Println("mode 3")
    }

    if((flag & mode4) == mode4){
        fmt.Println("mode 4")
    }
}

func main(){
    fmt.Println("啟用模式 1, 2")
    render(mode1 | mode2)
    fmt.Println("啟用模式 1, 3, 4")
    render(mode1 | mode3 | mode4)
}

執行結果:
啟用模式 1, 2
mode 1
mode 2
啟用模式 1, 3, 4
mode 1
mode 3
mode 4

利用 or 可以做聯級的效果 比如 mode1 | mode2 (0001 | 0010) 出來就變 0011

而在偵測時利用 and 可以做遮罩只針對特定的位元,比如現在 flag = abcdmode1 = 0001 則 abc 會變 0 , flag & mode1 會變成 000d 只看最後一位。

每次都要刷一排 0b 很麻煩,而且其實順序根本不重要,主要是要隨機賦予只有一個 bit 是 1 的二進位數,這時就可以利用 iota 啦

iota 改寫

package main
import "fmt"

const(
    mode1 = 1 << iota   // 1 << 0   0b0001
    mode2 = 1 << iota   // 1 << 1   0b0010
    mode3 = 1 << iota   // 1 << 2   0b0100
    mode4 = 1 << iota   // 1 << 3   0b1000
)

func render(flag int){
    if((flag & mode1) == mode1){
        fmt.Println("mode 1")
    }

    if((flag & mode2) == mode2){
        fmt.Println("mode 2")
    }

    if((flag & mode3) == mode3){
        fmt.Println("mode 3")
    }

    if((flag & mode4) == mode4){
        fmt.Println("mode 4")
    }
}

func main(){
    fmt.Println("啟用模式 1, 2")
    render(mode1 | mode2)
    fmt.Println("啟用模式 1, 3, 4")
    render(mode1 | mode3 | mode4)
}

iota 簡寫

當然也可以做簡寫

const(
    mode1 = 1 << iota
    mode2
    mode3
    mode4
)

後記

本文圖片來自:


上一篇
#19 Select & Multiplexing 選擇通道與多路複用 | Golang魔法使
下一篇
#21 鏈結串列 Linked-list | Golang魔法使
系列文
Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言