08 4월 2025
크고 빠르며 안정적인 소프트웨어를 구축하는 것은 종종 복잡성을 다루는 저글링처럼 느껴집니다. 만약 이러한 복잡성을 단순화하고, 성능 저하 없이 속도와 간단한 동시성을 제공하도록 처음부터 설계된 언어가 있다면 어떨까요? 바로 Go(종종 Golang이라고도 불림)입니다. Go는 현대 소프트웨어 개발, 특히 대규모 개발의 어려움을 직접적으로 해결하기 위해 고안된 프로그래밍 언어입니다. 단순성, 효율성, 동시성 프로그래밍을 우선시하며 개발자의 생산성을 크게 높이는 것을 목표로 합니다. 이 Go 튜토리얼은 Go 프로그래밍 학습에 필요한 기본 개념을 안내하는 시작점 역할을 합니다.
Go는 2007년경 구글에서 시스템 프로그래밍 베테랑들에 의해 탄생했습니다. 그들은 자신들이 선호하는 언어들의 장점을 결합하고 싫어하는 복잡성(특히 C++에서 발견되는 것들)을 버리려고 했습니다. 2009년에 공개적으로 발표되고 2012년에 안정적인 1.0 버전에 도달한 Go는 소프트웨어 개발 커뮤니티에서 빠르게 인기를 얻었습니다.
Go를 정의하는 주요 특징은 다음과 같습니다:
gofmt
), 의존성 관리(go mod
), 테스트(go test
), 빌드(go build
) 등을 위한 훌륭한 명령줄 도구를 함께 제공하여 개발 프로세스를 간소화합니다.이 언어에는 Renée French가 디자인한 친근한 마스코트인 고퍼(Gopher)도 있으며, 이는 Go 커뮤니티의 상징이 되었습니다. 공식 명칭은 Go이지만, "Golang"이라는 용어는 초기 웹사이트 도메인(golang.org
) 때문에 생겨났으며, 특히 온라인 검색 시 여전히 흔한 별칭으로 사용됩니다.
Go 코드를 작성하기 전에 Go 컴파일러와 도구가 필요합니다. 공식 Go 웹사이트 go.dev를 방문하여 사용 중인 운영 체제(Windows, macOS, Linux)에 맞는 간단한 설치 지침을 따르십시오. 설치 프로그램은 go
와 같은 필요한 명령어를 설정합니다.
전통적인 첫 프로그램을 만들어 봅시다. hello.go
라는 파일을 만들고 다음 코드를 입력하거나 붙여넣으십시오:
package main
import "fmt"
// 이 함수는 실행이 시작되는 메인 함수입니다.
func main() {
// Println은 콘솔에 텍스트 한 줄을 출력합니다.
fmt.Println("안녕, 고퍼!")
}
이 간단한 Go 코드 예제를 분석해 봅시다:
package main
: 모든 Go 프로그램은 패키지 선언으로 시작합니다. main
패키지는 특별하며, 이 패키지가 실행 가능한 프로그램으로 컴파일되어야 함을 의미합니다.import "fmt"
: 이 줄은 Go 표준 라이브러리의 일부인 fmt
패키지를 가져옵니다. fmt
패키지는 콘솔에 텍스트를 출력하는 것과 같은 형식화된 입출력(I/O) 함수를 제공합니다.func main() { ... }
: main
함수를 정의합니다. 실행 가능한 Go 프로그램의 실행은 항상 main
패키지의 main
함수에서 시작됩니다.fmt.Println("안녕, 고퍼!")
: 가져온 fmt
패키지의 Println
함수를 호출합니다. Println
(Print Line)은 “안녕, 고퍼!” 텍스트 문자열을 콘솔에 출력하고 그 뒤에 개행 문자를 추가합니다.이 프로그램을 실행하려면 터미널이나 명령 프롬프트를 열고 hello.go
를 저장한 디렉터리로 이동한 다음 명령을 실행하십시오:
go run hello.go
콘솔에 다음과 같은 출력이 나타나야 합니다:
안녕, 고퍼!
축하합니다! 첫 Go 프로그램을 성공적으로 실행했습니다.
첫 프로그램이 성공적으로 실행되었으니, Go 언어의 기본적인 구성 요소를 살펴보겠습니다. 이 섹션은 초보자를 위한 Go 튜토리얼 역할을 합니다.
변수는 프로그램 실행 중에 변경될 수 있는 데이터를 저장하는 데 사용됩니다. Go에서는 변수를 사용하기 전에 선언해야 하며, 이는 컴파일러가 타입 안전성을 보장하는 데 도움이 됩니다.
var
사용: var
키워드는 하나 이상의 변수를 선언하는 표준 방법입니다. 변수 이름 뒤에 타입을 명시적으로 지정할 수 있습니다.
package main
import "fmt"
func main() {
var greeting string = "Go에 오신 것을 환영합니다!" // 문자열 변수 선언
var score int = 100 // 정수 변수 선언
var pi float64 = 3.14159 // 64비트 부동 소수점 변수 선언
var isActive bool = true // 불리언 변수 선언
fmt.Println(greeting)
fmt.Println("초기 점수:", score)
fmt.Println("파이 근사값:", pi)
fmt.Println("활성 상태:", isActive)
}
짧은 변수 선언 :=
: 함수 내부에서 Go는 변수를 동시에 선언하고 초기화하는 간결한 :=
구문을 제공합니다. Go는 오른쪽에 할당된 값으로부터 변수의 타입을 자동으로 추론합니다.
package main
import "fmt"
func main() {
userName := "Gopher123" // Go는 'userName'이 문자열임을 추론
level := 5 // Go는 'level'이 int임을 추론
progress := 0.75 // Go는 'progress'가 float64임을 추론
fmt.Println("사용자 이름:", userName)
fmt.Println("레벨:", level)
fmt.Println("진행률:", progress)
}
중요 참고: :=
구문은 함수 내부에서만 사용할 수 있습니다. 패키지 수준(함수 외부)에서 선언된 변수의 경우 var
키워드를 사용해야 합니다.
제로 값 (Zero Values): 명시적인 초기값 없이 var
를 사용하여 변수를 선언하면 Go는 자동으로 제로 값을 할당합니다. 제로 값은 타입에 따라 다릅니다:
0
bool
)의 경우 false
string
타입의 경우 ""
(빈 문자열)nil
package main
import "fmt"
func main() {
var count int
var message string
var enabled bool
var userScore *int // 포인터 타입
var task func() // 함수 타입
fmt.Println("정수 제로 값:", count) // 출력: 정수 제로 값: 0
fmt.Println("문자열 제로 값:", message) // 출력: 문자열 제로 값:
fmt.Println("불리언 제로 값:", enabled) // 출력: 불리언 제로 값: false
fmt.Println("포인터 제로 값:", userScore) // 출력: 포인터 제로 값: <nil>
fmt.Println("함수 제로 값:", task) // 출력: 함수 제로 값: <nil>
}
Go는 여러 기본적인 내장 데이터 타입을 제공합니다:
int
, int8
, int16
, int32
, int64
, uint
, uint8
등): 정수를 나타냅니다. int
와 uint
는 플랫폼에 따라 다릅니다(보통 32비트 또는 64비트). 특정 크기가 필요할 때(예: 바이너리 데이터 형식 또는 성능 최적화) 명시적인 크기를 사용하십시오. uint8
은 byte
의 별칭입니다.float32
, float64
): 소수점이 있는 숫자를 나타냅니다. float64
가 기본이며 일반적으로 더 나은 정밀도를 위해 선호됩니다.bool
): 참 값을 나타내며, true
또는 false
중 하나입니다.string
): UTF-8로 인코딩된 문자 시퀀스를 나타냅니다. Go의 문자열은 불변(immutable)입니다 – 일단 생성되면 내용을 직접 변경할 수 없습니다. 문자열을 수정하는 것처럼 보이는 작업은 실제로는 새 문자열을 생성합니다.다음은 기본 타입을 사용하는 Go 예제입니다:
package main
import "fmt"
func main() {
item := "노트북" // string
quantity := 2 // int
price := 1250.75 // float64 (추론됨)
inStock := true // bool
// Go는 다른 숫자 타입 간의 명시적 타입 변환을 요구합니다.
totalCost := float64(quantity) * price // 곱셈을 위해 int 'quantity'를 float64로 변환
fmt.Println("상품:", item)
fmt.Println("수량:", quantity)
fmt.Println("단가:", price)
fmt.Println("재고 있음:", inStock)
fmt.Println("총 비용:", totalCost)
}
이 예제는 타입 추론을 사용한 변수 선언과 다른 숫자 타입으로 산술 연산을 수행할 때 명시적 타입 변환의 필요성을 보여줍니다.
상수는 변수와 유사하게 이름에 값을 바인딩하지만, 그 값은 컴파일 타임에 고정되며 프로그램 실행 중에 변경할 수 없습니다. const
키워드를 사용하여 선언됩니다.
package main
import "fmt"
const AppVersion = "1.0.2" // 문자열 상수
const MaxConnections = 1000 // 정수 상수
const Pi = 3.14159 // 부동 소수점 상수
func main() {
fmt.Println("애플리케이션 버전:", AppVersion)
fmt.Println("허용되는 최대 연결 수:", MaxConnections)
fmt.Println("파이 값:", Pi)
}
Go는 또한 증가하는 정수 상수의 정의를 단순화하는 특별한 키워드 iota
를 제공합니다. 이는 열거형 타입(enums)을 만드는 데 일반적으로 사용됩니다. iota
는 const
블록 내에서 0부터 시작하며 해당 블록의 각 후속 상수 선언마다 1씩 증가합니다.
package main
import "fmt"
// int를 기반으로 하는 사용자 정의 타입 LogLevel 정의
type LogLevel int
const (
Debug LogLevel = iota // 0
Info // 1 (iota 증가)
Warning // 2
Error // 3
)
func main() {
currentLevel := Info
fmt.Println("현재 로그 레벨:", currentLevel) // 출력: 현재 로그 레벨: 1
fmt.Println("오류 레벨:", Error) // 출력: 오류 레벨: 3
}
제어 흐름 문은 코드 문장이 실행되는 순서를 결정합니다.
if / else if / else
: 불리언 표현식에 따라 조건부로 코드 블록을 실행합니다. Go에서는 조건 주위에 괄호 ()
를 사용하지 않지만, 중괄호 {}
는 단일 문장 블록이라도 항상 필요합니다.
package main
import "fmt"
func main() {
temperature := 25
if temperature > 30 {
fmt.Println("꽤 덥습니다.")
} else if temperature < 10 {
fmt.Println("꽤 춥습니다.")
} else {
fmt.Println("온도가 적당합니다.") // 이것이 출력될 것입니다
}
// 짧은 문장이 조건 앞에 올 수 있으며, 거기서 선언된 변수는
// if/else 블록으로 범위가 제한됩니다.
if limit := 100; temperature < limit {
fmt.Printf("온도 %d는 제한 %d 미만입니다.\n", temperature, limit)
} else {
fmt.Printf("온도 %d는 제한 %d 미만이 아닙니다.\n", temperature, limit)
}
}
for
: Go에는 다용도의 for
루프라는 단 하나의 반복 구조만 있습니다. 다른 언어에서 익숙한 여러 방식으로 사용할 수 있습니다:
for
루프 (초기화; 조건; 후처리):
for i := 0; i < 5; i++ {
fmt.Println("반복:", i)
}
while
루프처럼 작동):
sum := 1
for sum < 100 { // sum이 100보다 작은 동안 반복
sum += sum
}
fmt.Println("최종 합계:", sum) // 출력: 최종 합계: 128
break
또는 return
을 사용하여 종료):
count := 0
for {
fmt.Println("반복 중...")
count++
if count > 3 {
break // 루프 종료
}
}
for...range
: 슬라이스, 배열, 맵, 문자열, 채널과 같은 데이터 구조의 요소를 반복합니다. 각 요소에 대한 인덱스/키와 값을 제공합니다.
colors := []string{"빨강", "초록", "파랑"}
// 인덱스와 값 모두 얻기
for index, color := range colors {
fmt.Printf("인덱스: %d, 색상: %s\n", index, color)
}
// 값만 필요한 경우, 빈 식별자 _를 사용하여 인덱스 무시
fmt.Println("색상 목록:")
for _, color := range colors {
fmt.Println("- ", color)
}
// 문자열의 문자(룬) 반복
for i, r := range "Go!" {
fmt.Printf("인덱스 %d, 룬 %c\n", i, r)
}
switch
: 긴 if-else if
체인에 대한 더 깔끔한 대안을 제공하는 다방향 조건문입니다. Go의 switch
는 많은 C 계열 언어보다 유연합니다:
break
필요 없음).switch
를 사용할 수 있습니다 (true
를 케이스 표현식과 비교).package main
import (
"fmt"
"time"
)
func main() {
day := time.Now().Weekday()
fmt.Println("오늘은:", day) // 예: 오늘은: Tuesday
switch day {
case time.Saturday, time.Sunday: // 한 케이스에 여러 값
fmt.Println("주말입니다!")
case time.Monday:
fmt.Println("주중의 시작입니다.")
default: // 선택적 기본 케이스
fmt.Println("평일입니다.")
}
// 표현식 없는 스위치는 깔끔한 if/else if 체인처럼 작동
hour := time.Now().Hour()
switch { // 암시적으로 'true'에 대해 스위칭
case hour < 12:
fmt.Println("좋은 아침입니다!")
case hour < 17:
fmt.Println("좋은 오후입니다!")
default:
fmt.Println("좋은 저녁입니다!")
}
}
Go는 몇 가지 필수적인 데이터 구조에 대한 내장 지원을 제공합니다.
Go의 배열은 선언 시 결정되는 고정된 크기를 갖습니다. 크기는 배열 타입의 일부입니다 ([3]int
는 [4]int
와 다른 타입입니다).
package main
import "fmt"
func main() {
// 3개의 정수 배열 선언. 제로 값(0)으로 초기화됩니다.
var numbers [3]int
numbers[0] = 10
numbers[1] = 20
// numbers[2]는 0 (제로 값)으로 유지됩니다.
fmt.Println("숫자들:", numbers) // 출력: 숫자들: [10 20 0]
fmt.Println("길이:", len(numbers)) // 출력: 길이: 3
// 배열을 인라인으로 선언하고 초기화
primes := [5]int{2, 3, 5, 7, 11}
fmt.Println("소수들:", primes) // 출력: 소수들: [2 3 5 7 11]
// 컴파일러가 ...을 사용하여 요소 수를 세도록 함
vowels := [...]string{"a", "e", "i", "o", "u"}
fmt.Println("모음들:", vowels, "길이:", len(vowels)) // 출력: 모음들: [a e i o u] 길이: 5
}
배열은 (예: 크기가 정말 고정되어 있고 알려진 경우) 용도가 있지만, 유연성 때문에 Go에서는 슬라이스가 훨씬 더 일반적으로 사용됩니다.
슬라이스는 Go에서 시퀀스를 위한 주력 데이터 구조입니다. 배열보다 더 강력하고 유연하며 편리한 인터페이스를 제공합니다. 슬라이스는 동적으로 크기가 조절되고, 기본 배열(underlying array)에 대한 변경 가능한 뷰입니다.
package main
import "fmt"
func main() {
// make(타입, 길이, 용량)을 사용하여 문자열 슬라이스 생성
// 용량은 선택 사항이며, 생략하면 길이와 동일하게 기본 설정됩니다.
// 길이: 슬라이스가 현재 포함하는 요소의 수.
// 용량: 기본 배열의 요소 수 (슬라이스의 첫 번째 요소부터 시작).
names := make([]string, 2, 5) // 길이 2, 용량 5
names[0] = "Alice"
names[1] = "Bob"
fmt.Println("초기 이름:", names, "길이:", len(names), "용량:", cap(names)) // 출력: 초기 이름: [Alice Bob] 길이: 2 용량: 5
// Append는 요소들을 끝에 추가합니다. 길이가 용량을 초과하면,
// 더 큰 새 기본 배열이 할당되고 슬라이스는 그것을 가리킵니다.
names = append(names, "Charlie")
names = append(names, "David", "Eve") // 여러 요소를 추가할 수 있음
fmt.Println("추가된 이름:", names, "길이:", len(names), "용량:", cap(names)) // 출력: 추가된 이름: [Alice Bob Charlie David Eve] 길이: 5 용량: 5 (또는 재할당된 경우 더 클 수 있음)
// 슬라이스 리터럴 (슬라이스와 기본 배열 생성)
scores := []int{95, 88, 72, 100}
fmt.Println("점수:", scores) // 출력: 점수: [95 88 72 100]
// 슬라이스 슬라이싱: *동일한* 기본 배열을 참조하는 새 슬라이스 헤더 생성.
// slice[low:high] - low 인덱스 요소 포함, high 인덱스 요소 제외.
topScores := scores[1:3] // 인덱스 1과 2의 요소 (값: 88, 72)
fmt.Println("상위 점수:", topScores) // 출력: 상위 점수: [88 72]
// 하위 슬라이스를 수정하면 원본 슬라이스(및 기본 배열)에 영향을 미침
topScores[0] = 90
fmt.Println("수정된 점수:", scores) // 출력: 수정된 점수: [95 90 72 100]
// 하한 생략 시 0으로 기본 설정, 상한 생략 시 길이로 기본 설정
firstTwo := scores[:2]
lastTwo := scores[2:]
fmt.Println("처음 두 개:", firstTwo) // 출력: 처음 두 개: [95 90]
fmt.Println("마지막 두 개:", lastTwo) // 출력: 마지막 두 개: [72 100]
}
주요 슬라이스 연산에는 len()
(현재 길이), cap()
(현재 용량), append()
(요소 추가), 그리고 [low:high]
구문을 사용한 슬라이싱이 포함됩니다.
맵은 Go의 내장 해시 테이블 또는 딕셔너리 구현입니다. 정렬되지 않은 키-값 쌍의 컬렉션을 저장하며, 모든 키는 동일한 타입이어야 하고 모든 값도 동일한 타입이어야 합니다.
package main
import "fmt"
func main() {
// make를 사용하여 문자열 키와 int 값을 갖는 빈 맵 생성
ages := make(map[string]int)
// 키-값 쌍 설정
ages["Alice"] = 30
ages["Bob"] = 25
ages["Charlie"] = 35
fmt.Println("Ages 맵:", ages) // 출력: Ages 맵: map[Alice:30 Bob:25 Charlie:35] (순서 보장 안 됨)
// 키를 사용하여 값 가져오기
aliceAge := ages["Alice"]
fmt.Println("Alice의 나이:", aliceAge) // 출력: Alice의 나이: 30
// 존재하지 않는 키의 값을 가져오면 값 타입의 제로 값을 반환 (int의 경우 0)
davidAge := ages["David"]
fmt.Println("David의 나이:", davidAge) // 출력: David의 나이: 0
// 키-값 쌍 삭제
delete(ages, "Bob")
fmt.Println("Bob 삭제 후:", ages) // 출력: Bob 삭제 후: map[Alice:30 Charlie:35]
// 두 값 할당 형식을 사용하여 키 존재 여부 확인
// 맵 키에 접근할 때 선택적으로 두 번째 불리언 값을 얻을 수 있습니다:
// 1. 값 (키가 없으면 제로 값)
// 2. 불리언: 키가 있으면 true, 없으면 false
val, exists := ages["Bob"] // 값이 필요하지 않으면 빈 식별자 _ 사용 (예: _, exists := ...)
fmt.Printf("Bob이 존재하나요? %t, 값: %d\n", exists, val) // 출력: Bob이 존재하나요? false, 값: 0
charlieAge, charlieExists := ages["Charlie"]
fmt.Printf("Charlie가 존재하나요? %t, 나이: %d\n", charlieExists, charlieAge) // 출력: Charlie가 존재하나요? true, 나이: 35
// 맵 선언 및 초기화를 위한 맵 리터럴
capitals := map[string]string{
"France": "Paris",
"Japan": "Tokyo",
"USA": "Washington D.C.",
}
fmt.Println("수도:", capitals)
}
함수는 코드를 재사용 가능한 단위로 구성하는 기본적인 빌딩 블록입니다. func
키워드를 사용하여 선언됩니다.
package main
import (
"fmt"
"errors" // 오류 값을 생성하기 위한 표준 라이브러리 패키지
)
// 두 개의 int 매개변수를 받아 그 합계를 int로 반환하는 간단한 함수.
// 매개변수 타입은 이름 뒤에 따릅니다: func funcName(param1 type1, param2 type2) returnType { ... }
func add(x int, y int) int {
return x + y
}
// 연속된 매개변수가 동일한 타입을 갖는 경우, 마지막 매개변수를 제외하고
// 타입을 생략할 수 있습니다.
func multiply(x, y int) int {
return x * y
}
// Go 함수는 여러 값을 반환할 수 있습니다. 이는 결과와 오류 상태를
// 동시에 반환하는 관용적인 방법입니다.
func divide(numerator float64, denominator float64) (float64, error) {
if denominator == 0 {
// 분모가 0이면 새 오류 값을 생성하고 반환합니다.
return 0, errors.New("0으로 나누는 것은 허용되지 않습니다")
}
// 성공하면 계산된 결과와 오류에 대해 'nil'을 반환합니다.
// 'nil'은 오류 타입(및 포인터, 슬라이스, 맵 등과 같은 다른 타입)의 제로 값입니다.
return numerator / denominator, nil
}
func main() {
sum := add(15, 7)
fmt.Println("합계:", sum) // 출력: 합계: 22
product := multiply(6, 7)
fmt.Println("곱:", product) // 출력: 곱: 42
// 여러 값을 반환하는 함수 호출
result, err := divide(10.0, 2.0)
// 항상 오류 값을 즉시 확인합니다.
if err != nil {
fmt.Println("오류:", err)
} else {
fmt.Println("나눗셈 결과:", result) // 출력: 나눗셈 결과: 5
}
// 잘못된 입력으로 다시 호출
result2, err2 := divide(5.0, 0.0)
if err2 != nil {
fmt.Println("오류:", err2) // 출력: 오류: 0으로 나누는 것은 허용되지 않습니다
} else {
fmt.Println("나눗셈 결과 2:", result2)
}
}
Go 함수가 여러 값을 반환하는 능력은 명시적인 오류 처리 메커니즘에 매우 중요합니다.
Go 코드는 패키지로 구성됩니다. 패키지는 함께 컴파일되는 단일 디렉터리에 위치한 소스 파일(.go
파일)의 모음입니다. 패키지는 코드 재사용성과 모듈성을 증진합니다.
package packageName
선언으로 시작해야 합니다. 같은 디렉터리의 파일들은 같은 패키지에 속해야 합니다. main
패키지는 특별하며, 실행 가능한 프로그램을 나타냅니다.import
키워드를 사용합니다. 표준 라이브러리 패키지는 짧은 이름으로 임포트합니다(예: "fmt"
, "math"
, "os"
). 외부 패키지는 일반적으로 소스 저장소 URL 기반의 경로를 사용합니다(예: "github.com/gin-gonic/gin"
).
import (
"fmt" // 표준 라이브러리
"math/rand" // math의 하위 패키지
"os"
// myExtPkg "github.com/someuser/externalpackage" // 임포트에 별칭을 붙일 수 있음
)
go.mod
파일로 정의됩니다. 주요 명령어는 다음과 같습니다:
go mod init <module_path>
: 새 모듈을 초기화합니다 (go.mod
생성).go get <package_path>
: 의존성을 추가하거나 업데이트합니다.go mod tidy
: 코드 임포트를 기반으로 사용되지 않는 의존성을 제거하고 누락된 의존성을 추가합니다.동시성은 여러 작업이 동시에 실행되는 것처럼 관리하는 것을 포함합니다. Go는 Communicating Sequential Processes (CSP)에서 영감을 받은 강력하면서도 간단한 내장 동시성 기능을 가지고 있습니다.
고루틴 (Goroutines): 고루틴은 Go 런타임에 의해 실행되고 관리되는 독립적으로 실행되는 함수입니다. 극도로 가벼운 스레드라고 생각할 수 있습니다. 함수나 메서드 호출 앞에 go
키워드를 붙이기만 하면 고루틴을 시작할 수 있습니다.
채널 (Channels): 채널은 고루틴 간에 값을 보내고 받을 수 있는 타입이 지정된 통로로, 통신과 동기화를 가능하게 합니다.
ch := make(chan Type)
(예: make(chan string)
)ch <- value
variable := <-ch
(값이 보내질 때까지 이 작업은 블록됩니다)다음은 고루틴과 채널을 보여주는 매우 기본적인 Go 예제입니다:
package main
import (
"fmt"
"time"
)
// 이 함수는 고루틴으로 실행됩니다.
// 메시지와 메시지를 다시 보낼 채널을 받습니다.
func displayMessage(msg string, messages chan string) {
fmt.Println("고루틴 작업 중...")
time.Sleep(1 * time.Second) // 약간의 작업을 시뮬레이션
messages <- msg // 메시지를 채널로 보냄
fmt.Println("고루틴 완료.")
}
func main() {
// 문자열 값을 전송하는 채널 생성.
// 이것은 버퍼 없는 채널이며, 송수신 작업이 상대방이 준비될 때까지
// 블록된다는 의미입니다.
messageChannel := make(chan string)
// displayMessage 함수를 고루틴으로 시작
// 'go' 키워드는 이 호출을 논블로킹으로 만듭니다; main은 즉시 계속됩니다.
go displayMessage("핑!", messageChannel)
fmt.Println("메인 함수가 메시지를 기다리는 중...")
// 채널에서 메시지 받기.
// 이 작업은 고루틴이 messageChannel로 메시지를 보낼 때까지
// 메인 함수를 블록합니다.
receivedMsg := <-messageChannel
fmt.Println("메인 함수가 받은 메시지:", receivedMsg) // 출력 (약 1초 후): 메인 함수가 받은 메시지: 핑!
// 메인이 종료되기 전에 고루틴의 마지막 출력문이 나타나도록 함
time.Sleep(50 * time.Millisecond)
}
이 간단한 예제는 동시 작업을 시작하고 채널을 통해 안전하게 결과를 받는 방법을 보여줍니다. Go의 동시성 모델은 버퍼 있는 채널, 여러 채널을 처리하기 위한 강력한 select
문, sync
패키지의 동기화 기본 요소를 포함하는 깊이 있는 주제입니다.
Go는 예외를 사용하는 언어와 비교하여 오류 처리에 독특한 접근 방식을 취합니다. 오류는 일반 값으로 취급됩니다. 잠재적으로 실패할 수 있는 함수는 일반적으로 마지막 반환 값으로 error
인터페이스 타입을 반환합니다.
error
인터페이스는 단일 메서드 Error() string
을 가집니다.nil
오류 값은 성공을 나타냅니다.nil
이 아닌 오류 값은 실패를 나타내며, 값 자체는 일반적으로 오류에 대한 세부 정보를 포함합니다.package main
import (
"fmt"
"os"
)
func main() {
// 존재하지 않을 가능성이 높은 파일을 열려고 시도
file, err := os.Open("a_surely_non_existent_file.txt")
// 관용적인 오류 확인: err이 nil이 아닌지 확인
if err != nil {
fmt.Println("치명적 오류: 파일 열기 실패:", err)
// 오류를 적절하게 처리합니다. 여기서는 그냥 종료합니다.
// 실제 애플리케이션에서는 오류를 기록하거나, 현재 함수에서
// 반환하거나, 대체 방안을 시도할 수 있습니다.
return // main 함수 종료
}
// err이 nil이었다면 함수는 성공했습니다.
// 이제 'file' 변수를 안전하게 사용할 수 있습니다.
fmt.Println("파일 열기 성공!") // 이 오류 시나리오에서는 출력되지 않음
// 파일과 같은 리소스를 닫는 것은 중요합니다.
// 'defer'는 함수 호출(file.Close())을 감싸는 함수(main)가
// 반환되기 직전에 실행되도록 예약합니다.
defer file.Close()
// ... 파일 읽기 또는 쓰기 작업 진행 ...
fmt.Println("파일에 대한 작업 수행 중...")
}
이 명시적인 if err != nil
확인은 제어 흐름을 매우 명확하게 만들고 개발자가 잠재적인 실패를 적극적으로 고려하고 처리하도록 권장합니다. defer
문은 리소스를 안정적으로 정리하기 위해 오류 확인과 함께 자주 사용됩니다.
Go의 중요한 강점 중 하나는 표준 배포판에 포함된 훌륭하고 응집력 있는 도구입니다:
go run <filename.go>
: 단일 Go 소스 파일 또는 메인 패키지를 직접 컴파일하고 실행합니다. 빠른 테스트에 유용합니다.go build
: Go 패키지와 그 의존성을 컴파일합니다. 기본적으로 패키지가 main
이면 실행 파일을 빌드합니다.gofmt
: 공식 Go 스타일 가이드라인에 따라 Go 소스 코드를 자동으로 포맷합니다. 프로젝트와 개발자 간의 일관성을 보장합니다. gofmt -w .
를 사용하여 현재 디렉터리와 하위 디렉터리의 모든 Go 파일을 포맷합니다.go test
: 유닛 테스트와 벤치마크를 실행합니다. 테스트는 _test.go
파일에 위치합니다.go mod
: 의존성 관리를 위한 Go 모듈 도구 (예: go mod init
, go mod tidy
, go mod download
).go get <package_path>
: 현재 모듈에 새 의존성을 추가하거나 기존 의존성을 업데이트합니다.go vet
: 컴파일러가 잡지 못할 수 있는 의심스러운 구조와 잠재적 오류를 검사하는 정적 분석 도구입니다.go doc <package> [symbol]
: 패키지 또는 특정 심볼에 대한 문서를 표시합니다.이 통합된 도구는 빌드, 테스트, 포맷팅, 의존성 관리와 같은 일반적인 개발 작업을 상당히 단순화합니다.
Go는 현대 소프트웨어 개발에 매력적인 제안을 제시합니다: 특히 동시성 시스템, 네트워크 서비스, 대규모 애플리케이션 구축에 있어 단순성, 성능, 강력한 기능의 균형을 맞춘 언어입니다. 깔끔한 문법, 강력한 정적 타이핑, 가비지 컬렉션을 통한 자동 메모리 관리, 내장된 동시성 기본 요소, 포괄적인 표준 라이브러리, 그리고 훌륭한 도구는 더 빠른 개발 주기, 쉬운 유지보수, 그리고 더 안정적인 소프트웨어에 기여합니다. 이는 새로운 그린필드 프로젝트뿐만 아니라 성능, 동시성, 유지보수성이 핵심 목표인 기존 시스템의 코드를 포팅하거나 현대화하는 데에도 강력한 선택지가 됩니다.