關於 web service, unity, blogger 等軟體工程筆記

Go Reader Pattern 設計與實作

Edit icon 1 則留言
Go

最近在寫小專案所遇到的設計問題,其中一個目標是從檔案中讀取英文單字 (word),並且將其轉換成英文小寫,因此記錄如何在 Golang 設計此機制,並且不斷改進寫了三個版本,並撰寫額外的測試程式碼 (testing code) 進行驗證。

首先先定義 Token 類型來記錄該些英文單字,並仿造其他 io.Reader 樣式,宣告 TokenReader

import "io"

type Token []byte

type TokenReader interface {
    Read() (Token, error)
}

初始實作

第一次的實作中,假設輸入都只會是 ASCII characters,一次性載入所有資料。並且建立 processByte 函數,處理英文字元以及處理大小寫轉換。

import "io"

func NewTokenReader(r io.Reader) TokenReader {
   return &tokenReader{r: r}
}

type tokenReader struct {
   r io.Reader
   buf []byte
}

func (r *tokenReader) Read() (Token, error) {
   if r.buf == nil {
      b, err := ioutil.ReadAll(r.r)
      if err != nil {
         return nil, err
      }
      r.buf = b
   }

   temp := make([]byte, 0)
   for len(r.buf) > 0 {
      b := r.processByte(r.buf[0])
      r.buf = r.buf[1:]

      if b == 0 && len(temp) > 0 {
         return Token(temp), nil
      } else if b != 0 {
         temp = append(temp, b)
      }
   }
   if len(temp) > 0 {
      return Token(temp), nil
   }

   return nil, io.EOF
}

func (r tokenReader) processByte(b byte) byte {
   // ASCII only
   if b >= 'a' && b <= 'z' {
      return b
   } else if b >= 'A' && b <= 'Z' {
      return b + 32
   }
   return 0
}

準備測試

測試程式碼也是不可少,因此使用 bytes.NewReader 來建立 io.Reader,並輸入一長串包含換行符號的測試字串。撰寫 assertReadToken 函數處理測試判斷,程式碼顯得比較優雅。

import (
   "bytes"
   "io"
   "testing"
)

func TestReader(t *testing.T) {
   d := bytes.NewReader(([]byte)(`aWfv42Ghjt 134aet
ecvrR45 RES as
U\*uuu____`))
   r := NewTokenReader(d)

   assertReadToken(t, r, nil, "awfv")
   assertReadToken(t, r, nil, "ghjt")
   assertReadToken(t, r, nil, "aet")
   assertReadToken(t, r, nil, "ecvrr")
   assertReadToken(t, r, nil, "res")
   assertReadToken(t, r, nil, "as")
   assertReadToken(t, r, nil, "u")
   assertReadToken(t, r, nil, "uuu")
   assertReadToken(t, r, io.EOF, "")
   assertReadToken(t, r, io.EOF, "")
}

func assertReadToken(t *testing.T, reader TokenReader, expectedErr error, expectedStr string) {
   t.Helper()
   if token, err := reader.Read(); err != expectedErr {
      t.Errorf("Got error: %v, expected: %v", err, expectedErr)
   } else if expectedErr == nil {
      if s := token.String(); s != expectedStr {
         t.Errorf("Got token: %s, expected: %s", s, expectedStr)
      }
   }
}

考慮 Rune

在第一的版本的實作中,並沒有考慮其他 UTF-8 字元的存在,在某些測試文字中可能會發生錯誤,因此修改原先實作,使用 utf8.DecodeRune 來取得 rune (符文,可想像是單一字元符號)。

此外也修改原先的 processByte 實作,改處理 rune。至於 rune 如何轉換成 []byte,則是使用 Golang 內部機制,先轉換成 string,然後再轉 []byte

import "io"

func NewTokenReader(r io.Reader) TokenReader {
   return &tokenReader{r: r}
}

type tokenReader struct {
   r   io.Reader
   buf []byte
}

func (r *tokenReader) Read() (Token, error) {
   if r.buf == nil {
      b, err := ioutil.ReadAll(r.r)
      if err != nil {
         return nil, err
      }
      r.buf = b
   }

   temp := make([]byte, 0)
   for len(r.buf) > 0 {
      rv, size := utf8.DecodeRune(r.buf)
      rv = r.processRune(rv)
      r.buf = r.buf[size:]

      if rv == 0 && len(temp) > 0 {
         return Token(temp), nil
      } else if rv != 0 {
         temp = append(temp, []byte(string(rv))...)
      }
   }
   if len(temp) > 0 {
      return Token(temp), nil
   }

   return nil, io.EOF
}

func (r tokenReader) processRune(rv rune) rune {
   if rv >= 'a' && rv <= 'z' {
      return b
   } else if rv >= 'A' && rv <= 'Z' {
      return rv + 32
   }
   return 0
}

考慮 Streaming 以及 Rune

最後考慮實作 Streaming read 的機制,畢竟記憶體可能無法一次載入大檔案資料,分成多次讀取與批次處理。

幸好 bufio.NewReader 已實作緩衝機制 (buffered),而且還提供 ReadRune 的實作,改寫起來相當輕鬆寫意。

import (
   "bufio"
   "io"
)

func NewTokenReader(r io.Reader) TokenReader {
   return &tokenReader{bufio.NewReader(r)}
}

type tokenReader struct {
   r *bufio.Reader
}

func (r tokenReader) Read() (Token, error) {
   temp := make([]byte, 0)
   for {
      rv, _, err := r.r.ReadRune()
      if err != nil {
         if err == io.EOF && len(temp) > 0 {
            return Token(temp), nil
         }
         return nil, err
      }

      rv = r.processRune(rv)
      if rv == 0 && len(temp) > 0 {
         return Token(temp), nil
      } else if rv != 0 {
         temp = append(temp, []byte(string(rv))...)
      }
   }
}

func (r tokenReader) processRune(rv rune) rune {
   if rv >= 'a' && rv <= 'z' {
      return rv
   } else if rv >= 'A' && rv <= 'Z' {
      return rv + 32
   }
   return 0
}

下一步呢?

開啟檔案,使用 TokenReader 的範例:

package main

import (
   "fmt"
   "io"
   "os"
)

func main() {

   file, err := os.Open("mobydick.txt")
   if err != nil {
      fmt.Fprintf(os.Stderr, "Cannot open file, %s\n", err.Error())
      os.Exit(1)
   }
   defer file.Close()

   reader := NewTokenReader(file)
   for {
      token, err := reader.Read()
      if err == io.EOF {
         break
      }
      fmt.Printf("%s\n", token)
   }
}

或是修改其 interface,以因應未來處理讀取不同的資料,例如以下改成採用 GPS 座標:

type GPSPoint struct {
    Latitude float64
    Longitude float64
    Timestamp time.Time
}

type TokenReader interface {
    Read() (GPSPoint, error)
}

延伸資料

1 則留言:

  1. 匿名1/24/2022

    casino web - deccasino.com
    Play fun88 vin online 카지노사이트 casino slots for real money in Mexico. There are no reviews on casino web sites for casino web gambling games. The main クイーンカジノ differences between casino web sites

    回覆刪除