본문으로 건너뛰기

Hello, World! — 첫 번째 Go 프로그램

Go 개발 환경이 준비되었다면, 이제 첫 번째 프로그램을 작성할 차례입니다. 단순한 Hello World 출력을 넘어, Go 프로그램의 구조, 패키지 시스템, 빌드 방법, import 심화까지 깊이 있게 다루겠습니다.

Hello, World! 전체 코드 분석​

package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

단 5줄의 코드이지만, Go의 핵심 개념이 모두 담겨 있습니다. 한 줄씩 분석해 보겠습니다.

package main​

모든 Go 파일의 첫 줄은 반드시 패키지 선언 입니다.

  • Go 프로그램은 패키지 단위로 구성됩니다
  • main 패키지는 특별합니다 — 실행 가능한(executable) 프로그램의 진입점임을 Go 컴파일러에 알립니다
  • 라이브러리 패키지는 package mylib처럼 다른 이름을 사용합니다
  • 같은 디렉토리 내의 모든 .go 파일은 동일한 패키지에 속해야 합니다

import "fmt"​

표준 라이브러리의 fmt 패키지를 가져옵니다.

  • fmt는 "format"의 약자로, 포매팅 및 입출력을 담당합니다
  • Go는 미사용 import를 컴파일 오류 로 처리합니다 — 불필요한 의존성을 방지하는 강력한 규칙입니다
  • import 경로는 문자열 리터럴로 표현됩니다

func main()​

  • func 키워드로 함수를 선언합니다
  • main() 함수는 main 패키지에서 프로그램 시작점(entry point)입니다
  • 매개변수와 반환값이 없습니다
  • 명령줄 인수는 os.Args로 접근합니다

fmt.Println("Hello, World!")​

  • fmt 패키지의 Println 함수 호출
  • 문자열을 출력하고 자동으로 줄바꿈(\n)을 추가합니다
  • fmt.Print (줄바꿈 없음), fmt.Printf (포매팅) 도 자주 사용합니다

파일 생성 및 실행​

# 프로젝트 디렉토리 생성
mkdir hello-go && cd hello-go
go mod init github.com/username/hello-go

# 파일 생성
cat > main.go << 'EOF'
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
fmt.Printf("Go 버전으로 출력: %s\n", "1.24")
fmt.Print("줄바꿈 없이 출력")
fmt.Print(" — 이어서 출력\n")
}
EOF

go run vs go build​

go run— 컴파일 + 실행을 한 번에 (개발 중 빠른 테스트용):

go run main.go
# 출력:
# Hello, World!
# Go 버전으로 출력: 1.24
# 줄바꿈 없이 출력 — 이어서 출력

go run은 임시 디렉토리에 바이너리를 생성하고 즉시 실행한 뒤 삭제합니다.

go build— 바이너리 파일 생성:

# 현재 디렉토리에 바이너리 빌드 (기본 이름: 모듈명 또는 디렉토리명)
go build

# 출력 파일 이름 지정
go build -o hello ./...

# 빌드 후 실행
./hello

차이점 정리:

항목go rungo build
용도개발 중 즉시 테스트배포용 바이너리 생성
속도빠름 (임시 빌드)약간 느림
바이너리 저장저장 안 함지정된 경로에 저장
디버깅 심볼포함기본 포함 (-ldflags="-s -w" 로 제거 가능)

크로스 컴파일​

Go의 강력한 기능 중 하나입니다. 현재 개발 환경과 다른 OS/아키텍처용 바이너리를 단일 명령으로 빌드합니다.

# Linux AMD64용 빌드 (macOS나 Windows에서도 가능)
GOOS=linux GOARCH=amd64 go build -o hello-linux ./...

# Windows 64비트용 빌드
GOOS=windows GOARCH=amd64 go build -o hello.exe ./...

# macOS Apple Silicon용 빌드
GOOS=darwin GOARCH=arm64 go build -o hello-mac-arm ./...

# 빌드 정보 포함 (버전 임베딩)
go build -ldflags="-X main.version=1.0.0" -o hello ./...

지원하는 GOOS/GOARCH 조합 확인:

go tool dist list

import 심화​

단일 vs 그룹 import​

// 단일 import
import "fmt"
import "os"
import "strings"

// 그룹 import (권장 — goimports가 자동 정렬)
import (
"fmt"
"os"
"strings"
)

Go 커뮤니티 관례는 다음 순서로 import를 그룹화합니다:

import (
// 1. 표준 라이브러리
"fmt"
"os"
"strings"

// 2. 외부 패키지 (blank line으로 구분)
"github.com/gin-gonic/gin"
"go.uber.org/zap"

// 3. 내부 패키지 (blank line으로 구분)
"github.com/username/myapp/internal/config"
"github.com/username/myapp/pkg/database"
)

별칭(Alias) import​

패키지 이름이 충돌하거나 너무 길 때 별칭을 사용합니다:

package main

import (
"fmt"
fm "fmt" // 별칭: fm으로 사용 (실제로는 비권장)
myfmt "fmt" // 또 다른 별칭 예시
)

func main() {
fmt.Println("fmt 사용")
fm.Println("별칭 fm으로도 동일하게 사용")
}

실제로 별칭이 유용한 경우:

import (
"crypto/rand"
mrand "math/rand" // 두 rand 패키지 동시 사용
)

// 이제 rand와 mrand로 구분
bytes := make([]byte, 16)
rand.Read(bytes) // crypto/rand
n := mrand.Intn(100) // math/rand

공백 import (_)​

패키지의 init() 함수를 실행하되, 패키지 이름을 직접 사용하지 않을 때 씁니다. 주로 드라이버 등록에 활용됩니다:

import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL 드라이버 등록
_ "github.com/go-sql-driver/mysql" // MySQL 드라이버 등록
)

func main() {
db, err := sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
// pq 패키지를 직접 쓰지 않아도 드라이버가 등록됨
_ = db
_ = err
}

점 import (.) — 사용하지 마세요​

import . "fmt"  // fmt의 모든 공개 이름을 현재 스코프로 가져옴

func main() {
Println("fmt. 없이 사용") // fmt.Println 대신
}

점 import는 코드를 읽을 때 어느 패키지에서 온 것인지 알 수 없어 가독성을 해치므로 테스트 코드 외에는 사용하지 않는 것이 원칙 입니다.

여러 파일로 구성된 패키지​

실제 프로젝트에서는 하나의 패키지를 여러 파일로 나눕니다.

디렉토리 구조:

hello-go/
go.mod
main.go
greet.go

greet.go:

package main

import "fmt"

// Greet는 이름을 받아 인사말을 출력합니다.
// 공개 함수는 대문자로 시작합니다.
func Greet(name string) {
fmt.Printf("안녕하세요, %s님!\n", name)
}

// greetLowercase는 패키지 내부에서만 사용하는 비공개 함수입니다.
func greetLowercase(name string) {
fmt.Printf("hello, %s\n", name)
}

main.go:

package main

import "fmt"

func main() {
fmt.Println("=== 인사말 프로그램 ===")
Greet("Go 학습자") // greet.go에 정의된 함수 호출
greetLowercase("내부 호출") // 같은 패키지이므로 접근 가능
}
# 두 파일을 모두 컴파일하여 실행
go run main.go greet.go

# 또는 패키지 전체 실행 (권장)
go run .

출력:

=== 인사말 프로그램 ===
안녕하세요, Go 학습자님!
hello, 내부 호출

Go의 공개/비공개 규칙:

  • 대문자로 시작: 패키지 외부에서 접근 가능 (exported)
  • 소문자로 시작: 패키지 내부에서만 접근 가능 (unexported)

go.mod와 go.sum 역할​

hello-go/
go.mod ← 모듈 정의 + 의존성 목록
go.sum ← 의존성 체크섬 (보안 검증)

go.mod: 프로젝트의 ID와 의존성 선언

module github.com/username/hello-go

go 1.24

require (
github.com/fatih/color v1.16.0
)

go.sum: 각 의존성의 SHA-256 해시 (변조 방지)

github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj/K049bybjyNeximgwfLJ+3X4MoO5Bi+5I=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=

외부 패키지 사용하기​

github.com/fatih/color 패키지로 컬러 출력 예제를 만들어 봅시다.

# 패키지 추가
go get github.com/fatih/color@v1.16.0

main.go:

package main

import (
"fmt"

"github.com/fatih/color"
)

func main() {
// 기본 컬러 출력
color.Red("에러: 파일을 찾을 수 없습니다")
color.Green("성공: 서버가 시작되었습니다")
color.Yellow("경고: 디스크 용량이 부족합니다")
color.Cyan("정보: 현재 버전은 1.24입니다")
color.Blue("디버그: 연결 시도 중...")

// 커스텀 스타일
bold := color.New(color.Bold)
bold.Println("굵은 텍스트")

boldRed := color.New(color.FgRed, color.Bold)
boldRed.Printf("중요 오류: %s\n", "데이터베이스 연결 실패")

// fmt와 혼용
fmt.Println(color.GreenString("✓ 테스트 통과"))
}
# 의존성 정리 후 실행
go mod tidy
go run .

프로그램 종료 코드​

Go 프로그램은 main() 함수가 정상적으로 종료되면 종료 코드 0을 반환합니다. 비정상 종료를 나타내려면 os.Exit을 사용합니다.

package main

import (
"fmt"
"os"
)

func main() {
args := os.Args // os.Args[0]은 프로그램 이름, [1]부터 인수

if len(args) < 2 {
fmt.Fprintln(os.Stderr, "사용법: hello <이름>")
os.Exit(1) // 비정상 종료 (0이 아닌 값)
}

name := args[1]
fmt.Printf("안녕하세요, %s!\n", name)
// os.Exit(0) — 정상 종료 (생략 가능)
}
go run main.go
# 출력: 사용법: hello <이름>
# 종료 코드: 1

go run main.go "Go개발자"
# 출력: 안녕하세요, Go개발자!
# 종료 코드: 0

# 종료 코드 확인
echo $?

주의: os.Exit은 defer된 함수를 실행하지 않으므로, 정리(cleanup) 로직이 있다면 defer를 사용하기 전에 호출해야 합니다.

실전 예제: 간단한 HTTP 서버​

표준 라이브러리만으로 HTTP 서버를 만들 수 있습니다.

package main

import (
"fmt"
"log"
"net/http"
"time"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s! 현재 시각: %s\n", name, time.Now().Format("2006-01-02 15:04:05"))
}

func main() {
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Go HTTP 서버에 오신 것을 환영합니다!")
fmt.Fprintln(w, "사용법: /hello?name=이름")
})

port := ":8080"
log.Printf("서버 시작: http://localhost%s\n", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal("서버 오류:", err)
}
}
go run main.go
# 브라우저에서 http://localhost:8080/hello?name=Gopher 접속

외부 패키지 없이 이 수준의 HTTP 서버를 작성할 수 있다는 것이 Go 표준 라이브러리의 강점입니다.

정리​

이 섹션에서 다룬 내용:

  • package main: 실행 가능 프로그램의 진입점 패키지
  • import: 단일, 그룹, 별칭, 공백, 점 import 방식과 용도
  • go run vs go build: 개발 중 테스트 vs 배포용 바이너리
  • 크로스 컴파일: GOOS/GOARCH로 다른 플랫폼용 빌드
  • 여러 파일 패키지: 대문자/소문자로 결정되는 공개/비공개 규칙
  • 외부 패키지 사용: go get, go.mod, go.sum 연동
  • os.Exit: 종료 코드 제어

다음 챕터에서는 Go의 변수, 타입, 상수를 깊이 있게 배워봅니다.