IT/Go

[Go/Golang] Golang의 문자열 읽기에 관한 고찰(Reader, Scanner, Scanln)

wookiist 2021. 3. 8. 11:25
728x90

Golang의 문자열 읽기에 관한 고찰

bufio 패키지의 bufio.NewReader(os.Stdin)

ReadLine() 메서드

ReadLine() 메서드는 os.Stdin으로부터 읽어 들인 데이터를 []Byte 형으로 리턴합니다. ReadLine 메서드를 사용할 땐 buffer의 크기 등 다양하게 고려해야 하는 케이스들이 많습니다. 이러한 이유에서 bufio.go 파일을 확인해보면 다음과 같은 내용이 있습니다.

ReadLine is a low-level line-reading primitive. Most callers should use
ReadBytes('\n') or ReadString('\n') instead or use a Scanner.

ReadLine 메서드는 저수준의 읽기 메서드이니, ReadBytes('\n') 또는 ReadString('\n') 을 사용하거나 Scanner를 사용하도록 하자.

그럼 ReadBytes(), ReadString() 메서드를 살펴보겠습니다.

ReadBytes() 메서드

func (b *Reader) ReadBytes(delim byte) ([]byte, error) {
    full, frag, n, err := b.collectFragments(delim)
    // Allocate new buffer to hold the full pieces and the fragment.
    buf := make([]byte, n)
    n = 0
    // Copy full pieces and fragment in.
    for i := range full {
        n += copy(buf[n:], full[i])
    }
    copy(buf[n:], frag)
    return buf, err
}

ReadBytes() 메서드는 delimeter를 인자로 받습니다. 표준 입력으로 들어오는 데이터를 delimeter까지 받아와서 []byte 형으로 리턴합니다. 따라서 문자열로 바로 사용할 수 없고, String() 으로 타입 캐스팅을 한 번 수행해주어야 합니다.

ReadLine() 메서드와 다른 점이라면 리턴된 값에 delimeter가 포함되어 있다는 점입니다. 따라서 문자열로 이용하려면, String() 을 이용한 타입 캐스팅뿐만 아니라, delimeter 제거 작업까지 수행해주셔야 합니다.

For simple uses, a Scanner may be more convenient.

간편하게 사용하기엔, Scanner가 더 편리할 것이다.

음, 간편한 방법이라고 Go의 코드에서 소개해준 Scanner를 바로 알아보고 싶지만 마지막으로 ReadString() 메서드를 알아보고 넘어갈까요?

ReadString() 메서드

ReadString() 메서드는 delimeter를 인자로 받아, 해당 delimeter까지의 입력을 받아들여 String 형으로 리턴하는 메서드입니다. 지금까지의 메서드와는 달리 String 자료형으로 반환된다는 점이 특별합니다.

func (b *Reader) ReadString(delim byte) (string, error) {
    full, frag, n, err := b.collectFragments(delim)
    // Allocate new buffer to hold the full pieces and the fragment.
    var buf strings.Builder
    buf.Grow(n)
    // Copy full pieces and fragment in.
    for _, fb := range full {
        buf.Write(fb)
    }
    buf.Write(frag)
    return buf.String(), err
}

그러나 ReadString()ReadBytes() 메서드와 마찬가지로 리턴되는 값에는 delimeter가 포함됩니다. 따라서 delimeter를 제거하는 추가적인 작업이 수반됩니다.

그래서 저는 ReadString()을 사용해서 문자열을 받아들일 땐, 다음과 같이 delimeter 처리를 해줍니다.

func main() {
    r := bufio.NewReader(os.Stdin)
    s, err := r.ReadString('\n') // delimeter로 '\n'을 설정하면, '\n'이 포함되어 있음.
    if err != nil {
        fmt.Fatalln(err)
    }
    s = strings.TrimSuffix(s, "\n") // 문자열에 포함된 '\n'을 제거해줌.
    fmt.Println(s)
}

For simple uses, a Scanner may be more convenient.

간편하게 사용하기엔, Scanner가 더 편리할 것이다.

그렇다면 Scanner는 어떤 장점이 있어서 ReadString() 메서드보다 편리하다는 것일까요?

bufio 패키지의 bufio.NewScanner(os.Stdin)

func NewScanner(r io.Reader) *Scanner
func (s *Scanner) Scan() bool
func (s *Scanner) Text() string

NewScanner() 메서드는 os.Stdin으로부터 문자를 읽어오는 Scanner를 생성, 반환합니다. ScannerScan() 메서드와 Text() 메서드를 이용해 문자를 읽고, 변수에 저장할 수 있습니다. 다음의 예제를 살펴보겠습니다.

func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Scan() // os.Stdin으로부터 "한 줄"을 읽어옵니다.
    s := sc.Text() // 읽어온 "한 줄"의 데이터를 Text() 메서드를 이용해 변수에 저장합니다.
    fmt.Println(s)
}

Scan() 메서드와 Text() 메서드를 조합하면, Reader로는 할 수 없었던 결과를 만들어낼 수 있습니다. delimeter가 제거된 String 자료형으로 결과가 리턴됩니다. 따라서 ReadString() 메서드에서처럼 strings.TrimSuffix() 처리를 하는 등의 부가적인 절차가 불필요합니다. 이러한 점 덕분에 많은 코드에서 Scanner를 사용하고 있습니다.

만약, 여러 줄로부터 읽어와서 하나의 슬라이스에 저장하는 작업이 필요하다면 다음처럼 사용하면 됩니다.

func main() {
    sc := bufio.NewScanner(os.Stdin)
    arr := make([]string, 0)
    for {
        sc.Scan()
        if s := sc.Text(); len(s) != 0 {
            arr = append(arr, s)
        } else {
            break
        }
    }
    fmt.Println(arr)
}

또는 다음과 같은 방법도 있습니다. 이 코드는 golang docs에 소개된 코드입니다.

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        fmt.Println(scanner.Text()) // Println will add back the final '\n'
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "reading standard input:", err)
    }
}

이외에도 다양한 방법으로 활용이 가능합니다. Scanner를 활용하는 방법을 추후 다뤄보도록 하겠습니다.

fmt 패키지의 Scan

Scanln() 메서드

func Scanln(a ...interface{}) (n int, err error)

Scanln() 메서드는 표준 입력으로부터 문자를 읽어올 때 사용하는 메서드입니다. 그러나 Scanln()은 한 줄만 읽어올 수 있다는 단점이 있으며, 여러 줄을 읽어올 땐 사용할 수 없습니다.

추가로 fmt.Scanln() 메서드와 Scanner.Scan()은 같은 코드에서 동시에 사용하시면 안 됩니다. 예상치 못한 버그나 에러가 발생할 수 있으며, 이를 디버깅하는 것보다 하나의 메커니즘(ScannerScanner만, Scanln이면 Scanln만)으로 통일시키는 작업이 훨씬 단순하고 빠르기 때문입니다.

TL;DR;

지금까지의 내용을 String 자료형 리턴 여부와 Delimeter 제거 여부로 정리해보자면 다음과 같습니다.

String 리턴 여부 Delimeter 제거 여부
ReadLine() X O
ReadBytes() X X
ReadString() O X
Scanner O O
fmt.Scanln() O X

참고

728x90
반응형