[Go] 표준 입/출력
개요
표준 입/출력(Standard I/O)은 프로그램이 사용자 또는 다른 프로그램과 데이터를 주고받는 기본적인 방법입니다. Go는 fmt, bufio 패키지를 통해 강력하고 유연한 입출력 기능을 제공합니다.
표준 스트림
- 표준 입력 (stdin):
os.Stdin- 키보드 또는 파이프로부터 입력 - 표준 출력 (stdout):
os.Stdout- 화면 또는 파이프로 출력 - 표준 에러 (stderr):
os.Stderr- 에러 메시지 출력
표준 출력 (Standard Output)
fmt 패키지 함수
fmt.Print
공백 없이 출력, 자동 줄바꿈 없음
package main
import "fmt"
func main() {
fmt.Print("Hello")
fmt.Print("World")
fmt.Print("!")
// 출력: HelloWorld!
}
fmt.Println
공백으로 구분하여 출력, 자동 줄바꿈
package main
import "fmt"
func main() {
fmt.Println("Hello", "World", "!")
fmt.Println(1, 2, 3)
// 출력:
// Hello World !
// 1 2 3
}
fmt.Printf
포맷 지정 출력
package main
import "fmt"
func main() {
name := "Alice"
age := 30
height := 165.5
fmt.Printf("이름: %s\n", name)
fmt.Printf("나이: %d살\n", age)
fmt.Printf("키: %.1fcm\n", height)
fmt.Printf("%s님은 %d살이고 키는 %.1fcm입니다.\n", name, age, height)
// 출력:
// 이름: Alice
// 나이: 30살
// 키: 165.5cm
// Alice님은 30살이고 키는 165.5cm입니다.
}
주요 포맷 동사 (Format Verbs)
일반 동사
%v // 기본 형식으로 값 출력
%+v // 구조체 필드명 포함
%#v // Go 문법 형식으로 출력
%T // 타입 출력
%% // 리터럴 % 출력
fmt.Printf("%v\n", 123) // 123
fmt.Printf("%+v\n", struct{X int}{42}) // {X:42}
fmt.Printf("%#v\n", "hello") // "hello"
fmt.Printf("%T\n", 3.14) // float64
fmt.Printf("100%%\n") // 100%
불린
%t // true 또는 false
fmt.Printf("%t\n", true) // true
fmt.Printf("%t\n", false) // false
정수
%d // 10진수
%b // 2진수
%o // 8진수
%x // 16진수 (소문자)
%X // 16진수 (대문자)
%c // 유니코드 문자
num := 42
fmt.Printf("%d\n", num) // 42
fmt.Printf("%b\n", num) // 101010
fmt.Printf("%o\n", num) // 52
fmt.Printf("%x\n", num) // 2a
fmt.Printf("%X\n", num) // 2A
fmt.Printf("%c\n", 65) // A
실수
%f // 소수점 표기
%e // 지수 표기 (소문자)
%E // 지수 표기 (대문자)
%g // 간결한 표기 (자동 선택)
f := 123.456789
fmt.Printf("%f\n", f) // 123.456789
fmt.Printf("%.2f\n", f) // 123.46 (소수점 2자리)
fmt.Printf("%e\n", f) // 1.234568e+02
fmt.Printf("%E\n", f) // 1.234568E+02
fmt.Printf("%g\n", f) // 123.456789
문자열
%s // 문자열
%q // 따옴표로 감싼 문자열
%x // 16진수 (바이트 단위)
s := "Hello"
fmt.Printf("%s\n", s) // Hello
fmt.Printf("%q\n", s) // "Hello"
fmt.Printf("%x\n", s) // 48656c6c6f
포인터
%p // 포인터 주소 (16진수)
num := 42
fmt.Printf("%p\n", &num) // 0xc0000b4010 (예시)
너비와 정밀도
%5d // 최소 5자리, 오른쪽 정렬
%-5d // 최소 5자리, 왼쪽 정렬
%05d // 최소 5자리, 0으로 채움
%.2f // 소수점 2자리
%8.2f // 총 8자리, 소수점 2자리
fmt.Printf("%5d\n", 42) // 42
fmt.Printf("%-5d|\n", 42) // 42 |
fmt.Printf("%05d\n", 42) // 00042
fmt.Printf("%.2f\n", 3.14159) // 3.14
fmt.Printf("%8.2f\n", 3.14) // 3.14
fmt.Sprint 계열 (문자열 반환)
s1 := fmt.Sprint("Hello", "World")
s2 := fmt.Sprintln("Hello", "World")
s3 := fmt.Sprintf("이름: %s, 나이: %d", "Alice", 30)
fmt.Println(s1) // HelloWorld
fmt.Println(s2) // Hello World\n
fmt.Println(s3) // 이름: Alice, 나이: 30
fmt.Fprint 계열 (Writer에 출력)
import (
"fmt"
"os"
)
// 표준 출력에 쓰기
fmt.Fprint(os.Stdout, "Hello")
fmt.Fprintln(os.Stdout, "World")
fmt.Fprintf(os.Stdout, "Number: %d\n", 42)
// 표준 에러에 쓰기
fmt.Fprintln(os.Stderr, "에러 메시지")
내장 print/println (비권장)
내장 함수 print와 println은 디버깅 용도로만 사용하며, 프로덕션 코드에서는 fmt 패키지 사용을 권장합니다.
print("Hello") // 출력되지만 포맷 지정 불가
println("World") // 줄바꿈만 추가
print(1, 2, 3) // 123 (공백 없음)
println(1, 2, 3) // 1 2 3 (공백으로 구분)
// ⚠️ 비권장: 표준 에러로 출력되며, 동작이 보장되지 않음
표준 입력 (Standard Input)
fmt 패키지 입력 함수
fmt.Scan
공백, 탭, 줄바꿈으로 구분된 값 읽기
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("이름과 나이 입력: ")
n, err := fmt.Scan(&name, &age)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Printf("읽은 값 개수: %d\n", n)
fmt.Printf("이름: %s, 나이: %d\n", name, age)
}
// 입력: Alice 30
// 출력:
// 읽은 값 개수: 2
// 이름: Alice, 나이: 30
fmt.Scanln
한 줄에서 공백으로 구분된 값 읽기 (줄바꿈까지)
package main
import "fmt"
func main() {
var num1, num2 int
fmt.Print("두 숫자 입력: ")
n, err := fmt.Scanln(&num1, &num2)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Printf("읽은 값: %d, %d (개수: %d)\n", num1, num2, n)
}
// 입력: 10 20
// 출력: 읽은 값: 10, 20 (개수: 2)
fmt.Scanf
포맷 지정 입력
package main
import "fmt"
func main() {
var year, month, day int
fmt.Print("날짜 입력 (YYYY-MM-DD): ")
n, err := fmt.Scanf("%d-%d-%d", &year, &month, &day)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Printf("읽은 값: %d년 %d월 %d일 (개수: %d)\n", year, month, day, n)
}
// 입력: 2024-12-25
// 출력: 읽은 값: 2024년 12월 25일 (개수: 3)
bufio 패키지 (권장)
bufio 패키지는 버퍼링된 입출력을 제공하여 성능이 우수하고 더 유연합니다.
bufio.Scanner (가장 권장)
한 줄씩 읽기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("이름 입력: ")
scanner.Scan() // 한 줄 읽기
name := scanner.Text()
fmt.Printf("안녕하세요, %s님!\n", name)
}
여러 줄 읽기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("여러 줄 입력 (빈 줄로 종료):")
lines := []string{}
for scanner.Scan() {
line := scanner.Text()
if line == "" {
break
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Println("\n입력된 내용:")
for i, line := range lines {
fmt.Printf("%d: %s\n", i+1, line)
}
}
공백으로 분리하여 읽기
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("공백으로 구분된 단어 입력: ")
scanner.Scan()
line := scanner.Text()
words := strings.Fields(line)
fmt.Printf("단어 개수: %d\n", len(words))
for i, word := range words {
fmt.Printf("%d: %s\n", i+1, word)
}
}
// 입력: Hello World Go Lang
// 출력:
// 단어 개수: 4
// 1: Hello
// 2: World
// 3: Go
// 4: Lang
토큰 단위로 읽기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords) // 단어 단위로 분리
fmt.Print("단어들 입력: ")
words := []string{}
for scanner.Scan() {
word := scanner.Text()
words = append(words, word)
if len(words) >= 3 { // 3개만 읽기
break
}
}
fmt.Println("읽은 단어:", words)
}
bufio.Reader
ReadString으로 한 줄 읽기
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("문장 입력: ")
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("읽기 오류:", err)
return
}
// 줄바꿈 제거
line = strings.TrimSpace(line)
fmt.Printf("입력: %s\n", line)
}
ReadBytes로 읽기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("입력: ")
bytes, err := reader.ReadBytes('\n')
if err != nil {
fmt.Println("읽기 오류:", err)
return
}
fmt.Printf("바이트: %v\n", bytes)
fmt.Printf("문자열: %s", string(bytes))
}
입력 방법 비교
| 방법 | 장점 | 단점 | 사용 상황 |
|---|---|---|---|
fmt.Scan |
간단, 타입 자동 변환 | 공백 처리 제한적 | 간단한 입력 |
fmt.Scanln |
한 줄 단위 읽기 | 포맷 유연성 낮음 | 고정 포맷 입력 |
fmt.Scanf |
포맷 지정 가능 | 복잡한 포맷 어려움 | 구조화된 입력 |
bufio.Scanner |
유연, 효율적 | 코드 약간 길어짐 | 대부분의 경우 권장 |
bufio.Reader |
세밀한 제어 | 저수준 API | 특수한 요구사항 |
실용 예제
예제 1: 기본 입출력
package main
import "fmt"
func main() {
var num1, num2 int
fmt.Print("두 숫자 입력: ")
n, err := fmt.Scanln(&num1, &num2)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Printf("읽은 값 개수: %d\n", n)
fmt.Printf("합: %d\n", num1+num2)
fmt.Printf("곱: %d\n", num1*num2)
}
// 입력: 5 3
// 출력:
// 읽은 값 개수: 2
// 합: 8
// 곱: 15
예제 2: 여러 값 입력 (bufio.Scanner 권장)
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("공백으로 구분된 숫자들 입력: ")
scanner.Scan()
line := scanner.Text()
numStrs := strings.Fields(line)
numbers := []int{}
for _, numStr := range numStrs {
num, err := strconv.Atoi(numStr)
if err != nil {
fmt.Printf("'%s'는 유효한 숫자가 아닙니다.\n", numStr)
continue
}
numbers = append(numbers, num)
}
sum := 0
for _, num := range numbers {
sum += num
}
fmt.Printf("입력된 숫자: %v\n", numbers)
fmt.Printf("합계: %d\n", sum)
}
// 입력: 1 2 3 4 5
// 출력:
// 입력된 숫자: [1 2 3 4 5]
// 합계: 15
예제 3: 구조화된 데이터 입력
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
type Person struct {
Name string
Age int
City string
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
people := []Person{}
fmt.Println("사람 정보 입력 (이름,나이,도시 형식, 빈 줄로 종료):")
for scanner.Scan() {
line := scanner.Text()
if line == "" {
break
}
parts := strings.Split(line, ",")
if len(parts) != 3 {
fmt.Println("잘못된 형식입니다. 다시 입력하세요.")
continue
}
age, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
fmt.Println("나이는 숫자여야 합니다.")
continue
}
person := Person{
Name: strings.TrimSpace(parts[0]),
Age: age,
City: strings.TrimSpace(parts[2]),
}
people = append(people, person)
}
fmt.Println("\n입력된 정보:")
for i, p := range people {
fmt.Printf("%d. %s (%d세, %s)\n", i+1, p.Name, p.Age, p.City)
}
}
// 입력:
// Alice,30,Seoul
// Bob,25,Busan
// Charlie,35,Incheon
// (빈 줄)
//
// 출력:
// 입력된 정보:
// 1. Alice (30세, Seoul)
// 2. Bob (25세, Busan)
// 3. Charlie (35세, Incheon)
예제 4: 대화형 프로그램
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("간단한 계산기 (덧셈만)")
fmt.Println("사용법: 숫자1 + 숫자2")
fmt.Println("종료: exit")
for {
fmt.Print("> ")
scanner.Scan()
input := scanner.Text()
if strings.ToLower(input) == "exit" {
fmt.Println("프로그램을 종료합니다.")
break
}
parts := strings.Split(input, "+")
if len(parts) != 2 {
fmt.Println("잘못된 형식입니다.")
continue
}
var num1, num2 int
n, err := fmt.Sscanf(input, "%d + %d", &num1, &num2)
if err != nil || n != 2 {
fmt.Println("숫자를 파싱할 수 없습니다.")
continue
}
fmt.Printf("결과: %d\n", num1+num2)
}
}
// 실행 예:
// > 5 + 3
// 결과: 8
// > 10 + 20
// 결과: 30
// > exit
// 프로그램을 종료합니다.
에러 처리
fmt 입력 함수 에러
package main
import (
"fmt"
"io"
)
func main() {
var num int
fmt.Print("숫자 입력: ")
_, err := fmt.Scan(&num)
if err != nil {
if err == io.EOF {
fmt.Println("입력 종료")
} else {
fmt.Printf("입력 오류: %v\n", err)
}
return
}
fmt.Printf("입력된 숫자: %d\n", num)
}
bufio.Scanner 에러
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
fmt.Println("입력:", line)
if line == "quit" {
break
}
}
// 스캔 중 에러 확인
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "스캔 오류: %v\n", err)
os.Exit(1)
}
}
입출력 리다이렉션
파일에서 입력 읽기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 명령행에서: go run main.go < input.txt
scanner := bufio.NewScanner(os.Stdin)
lineNum := 1
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("%d: %s\n", lineNum, line)
lineNum++
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "읽기 오류:", err)
}
}
파일로 출력 저장
package main
import (
"fmt"
)
func main() {
// 명령행에서: go run main.go > output.txt
fmt.Println("첫 번째 줄")
fmt.Println("두 번째 줄")
fmt.Println("세 번째 줄")
}
파이프 사용
# 다른 프로그램의 출력을 입력으로
echo "Hello World" | go run main.go
# 출력을 다른 프로그램으로
go run main.go | grep "pattern"
일반적인 실수
1. print/println 사용
// ❌ 나쁜 예
print("Hello")
println("World")
// ✅ 좋은 예
fmt.Print("Hello")
fmt.Println("World")
2. 입력 에러 무시
// ❌ 나쁜 예
var num int
fmt.Scan(&num) // 에러 무시
// ✅ 좋은 예
var num int
_, err := fmt.Scan(&num)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
3. 줄바꿈 문자 처리 실수
// ❌ 문제가 될 수 있음
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
// line에는 '\n'이 포함됨
// ✅ 좋은 예
import "strings"
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(line) // 공백 및 줄바꿈 제거
4. 버퍼 오버플로우
// ❌ 큰 입력 시 문제
scanner := bufio.NewScanner(os.Stdin)
// 기본 버퍼 크기: 64KB
// ✅ 큰 입력 처리
scanner := bufio.NewScanner(os.Stdin)
buf := make([]byte, 1024*1024) // 1MB 버퍼
scanner.Buffer(buf, 1024*1024)
5. 타입 변환 없이 사용
// ❌ 컴파일 에러
var s string
fmt.Scan(&s)
num := s // string을 int로 사용 불가
// ✅ 올바른 방법
import "strconv"
num, err := strconv.Atoi(s)
if err != nil {
fmt.Println("변환 오류:", err)
}
성능 고려사항
1. 대량 입력 시 bufio 사용
// 느림: fmt.Scan 반복
for i := 0; i < 100000; i++ {
var num int
fmt.Scan(&num)
}
// 빠름: bufio.Scanner
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
num, _ := strconv.Atoi(scanner.Text())
// 처리
}
2. 불필요한 문자열 할당 방지
// 비효율적
for i := 0; i < 1000; i++ {
s := fmt.Sprintf("Number: %d\n", i)
fmt.Print(s)
}
// 효율적
for i := 0; i < 1000; i++ {
fmt.Printf("Number: %d\n", i)
}
3. 버퍼링된 Writer 사용
import (
"bufio"
"fmt"
"os"
)
writer := bufio.NewWriter(os.Stdout)
for i := 0; i < 10000; i++ {
fmt.Fprintf(writer, "Line %d\n", i)
}
writer.Flush() // 버퍼 비우기
베스트 프랙티스
- fmt 패키지 사용:
print/println대신fmt.Print/fmt.Println - 에러 처리: 입력 함수의 에러는 항상 확인
- bufio 사용: 대량 입력/출력은
bufio패키지 활용 - 줄바꿈 처리:
strings.TrimSpace로 공백 제거 - 타입 안전성: 입력 값 타입 변환 시 에러 확인
- 버퍼 크기 조정: 큰 입력 예상 시 버퍼 크기 증가
- 명확한 프롬프트: 사용자에게 입력 형식 안내
- 검증: 입력 값 검증 로직 추가
- 리소스 정리: 파일 등 리소스 사용 시
defer Close() - 표준 스트림 구분: 일반 출력은
stdout, 에러는stderr
표준 에러 출력
package main
import (
"fmt"
"os"
)
func main() {
// 표준 출력
fmt.Println("일반 메시지")
fmt.Fprintln(os.Stdout, "표준 출력 메시지")
// 표준 에러
fmt.Fprintln(os.Stderr, "에러 메시지")
}
// 리다이렉션으로 분리 가능
// go run main.go > stdout.txt 2> stderr.txt
종합 예제
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 1. 기본 출력
fmt.Println("=== 표준 입출력 예제 ===")
// 2. 정수 두 개 입력
fmt.Print("두 정수 입력 (공백으로 구분): ")
scanner.Scan()
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) < 2 {
fmt.Fprintln(os.Stderr, "두 개의 숫자를 입력해야 합니다.")
return
}
num1, err1 := strconv.Atoi(parts[0])
num2, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
fmt.Fprintln(os.Stderr, "유효한 숫자를 입력하세요.")
return
}
// 3. 포맷 출력
fmt.Printf("\n입력된 숫자: %d, %d\n", num1, num2)
fmt.Printf("합: %d\n", num1+num2)
fmt.Printf("차: %d\n", num1-num2)
fmt.Printf("곱: %d\n", num1*num2)
if num2 != 0 {
fmt.Printf("몫: %.2f\n", float64(num1)/float64(num2))
} else {
fmt.Println("0으로 나눌 수 없습니다.")
}
// 4. 스캐너 에러 확인
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "입력 오류: %v\n", err)
os.Exit(1)
}
}
실행 예시
=== 표준 입출력 예제 ===
두 정수 입력 (공백으로 구분): 15 4
입력된 숫자: 15, 4
합: 19
차: 11
곱: 60
몫: 3.75