Updated:

11 minute read

개요

표준 입/출력(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 (비권장)

내장 함수 printprintln은 디버깅 용도로만 사용하며, 프로덕션 코드에서는 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()  // 버퍼 비우기


베스트 프랙티스

  1. fmt 패키지 사용: print/println 대신 fmt.Print/fmt.Println
  2. 에러 처리: 입력 함수의 에러는 항상 확인
  3. bufio 사용: 대량 입력/출력은 bufio 패키지 활용
  4. 줄바꿈 처리: strings.TrimSpace로 공백 제거
  5. 타입 안전성: 입력 값 타입 변환 시 에러 확인
  6. 버퍼 크기 조정: 큰 입력 예상 시 버퍼 크기 증가
  7. 명확한 프롬프트: 사용자에게 입력 형식 안내
  8. 검증: 입력 값 검증 로직 추가
  9. 리소스 정리: 파일 등 리소스 사용 시 defer Close()
  10. 표준 스트림 구분: 일반 출력은 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


참고 자료