출시·고도화 중
Go 안내서 · 3/6
Go는 정적 타입 언어이지만 타입 추론 덕분에 코드가 장황하지 않습니다. 이 장에서는 기본 타입부터 슬라이스, 맵, 구조체, 메서드, 인터페이스, 포인터까지 Go 프로그램을 이루는 데이터 표현을 다룹니다.
| 분류 | 타입 |
|---|---|
| 불리언 | bool |
| 정수 | int, int8~int64, uint, uint8~uint64 |
| 실수·복소수 | float32, float64, complex64, complex128 |
| 문자열 | string (UTF-8 바이트의 읽기 전용 나열) |
| 별칭 | byte(= uint8), rune(= int32, 유니코드 코드 포인트) |
Go는 암묵적인 형 변환을 하지 않습니다. int와 float64를 더하려면 float64(n)처럼 명시적으로 바꿔야 합니다. 문자열을 for range로 돌면 바이트가 아니라 rune 단위로 나옵니다.
n := 3
f := 1.5
total := float64(n) + f
s := "고퍼"
fmt.Println(len(s)) // 6: 바이트 길이
for i, r := range s {
fmt.Println(i, string(r)) // 0 고, 3 퍼
}배열은 길이가 타입의 일부인 고정 크기 값입니다([3]int). 실제로는 배열보다 슬라이스를 훨씬 많이 씁니다. 슬라이스는 밑에 있는 배열의 일부를 가리키는 가벼운 보기이며, 길이(len)와 용량(cap)을 가집니다. append는 용량이 부족하면 더 큰 배열을 새로 잡으므로 결과를 꼭 다시 받아야 합니다.
arr := [3]int{1, 2, 3} // 배열
nums := []int{1, 2, 3} // 슬라이스
nums = append(nums, 4, 5)
part := nums[1:3] // [2 3], nums 와 같은 배열을 공유
part[0] = 20 // nums[1] 도 20 이 된다
buf := make([]byte, 0, 64) // 길이 0, 용량 64
copied := make([]int, len(nums))
copy(copied, nums) // 공유하지 않는 복사본
fmt.Println(arr, nums, len(buf), cap(buf), copied)부분 슬라이스는 원본과 메모리를 공유하므로, 독립된 값이 필요하면 copy나 slices.Clone으로 복사합니다. 표준 라이브러리의 slices 패키지에는 slices.Sort, slices.Contains, slices.Index 같은 도우미 함수가 있습니다.
맵은 키와 값의 해시 테이블입니다. 없는 키를 읽으면 값 타입의 제로 값이 나오므로, 존재 여부를 알아야 할 때는 두 번째 반환값(관례상 ok)을 확인합니다. 맵의 순회 순서는 정해져 있지 않습니다.
ages := map[string]int{"alice": 31, "bob": 27}
ages["carol"] = 40
if age, ok := ages["dave"]; !ok {
fmt.Println("dave 없음", age) // age 는 0
}
delete(ages, "bob")
for name, age := range ages {
fmt.Println(name, age)
}
counts := make(map[string]int)
counts["go"]++ // 없는 키도 제로 값에서 시작하므로 바로 더할 수 있다nil 맵은 읽을 수는 있지만 쓰면 패닉이 납니다. 맵을 만들 때는 리터럴이나 make를 씁니다.
구조체는 이름 붙은 필드의 묶음입니다. Go에는 클래스가 없고, 대신 타입에 메서드를 붙입니다. 메서드는 수신자(receiver)를 가진 함수입니다. 값을 바꾸는 메서드는 포인터 수신자(*T)를, 읽기만 하는 메서드는 값 수신자를 쓰는 것이 일반적입니다. 한 타입 안에서는 수신자 종류를 통일하는 편이 좋습니다.
type Point struct {
X, Y float64
}
func (p Point) Dist() float64 { // 값 수신자: 복사본을 받는다
return math.Hypot(p.X, p.Y)
}
func (p *Point) Scale(k float64) { // 포인터 수신자: 원본을 바꾼다
p.X *= k
p.Y *= k
}
type Named struct {
Point // 임베딩: Point 의 필드와 메서드를 그대로 쓴다
Name string
}
func main() {
p := Point{X: 3, Y: 4}
p.Scale(2) // Go 가 &p 로 바꿔 호출한다
n := Named{Point: p, Name: "A"}
fmt.Println(p.Dist(), n.X, n.Dist())
}상속 대신 임베딩으로 다른 타입의 필드와 메서드를 끌어옵니다. 이는 "is-a"보다 "has-a" 조합에 가깝습니다.
인터페이스는 메서드 집합으로 정의되는 타입입니다. Go에서는 implements 선언이 없습니다. 필요한 메서드를 모두 가진 타입은 자동으로 그 인터페이스를 만족합니다. 그래서 인터페이스는 쓰는 쪽에서 작게 정의하는 것이 관례입니다.
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
type Circle struct{ R float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func describe(v any) string {
switch x := v.(type) { // 타입 스위치
case Shape:
return fmt.Sprintf("넓이 %.2f", x.Area())
case string:
return "문자열 " + x
default:
return "알 수 없음"
}
}
func main() {
shapes := []Shape{Rect{2, 3}, Circle{1}}
for _, s := range shapes {
fmt.Println(describe(s))
}
}any는 빈 인터페이스 interface{}의 별칭으로 모든 값을 담을 수 있습니다. 담긴 실제 타입은 타입 단언 v.(T)나 타입 스위치로 꺼냅니다. 타입 매개변수(제네릭)도 지원하므로, 여러 타입에 같은 알고리즘을 쓸 때는 func Max[T cmp.Ordered](a, b T) T처럼 쓸 수 있습니다.
포인터는 값의 주소를 담습니다. &x는 주소를 얻고 *p는 그 주소의 값을 읽거나 씁니다. 포인터 연산은 없으며 메모리는 가비지 컬렉터가 관리하므로, 지역 변수의 주소를 돌려줘도 안전합니다.
func newCounter() *int {
c := 0
return &c // 함수가 끝나도 안전하다
}
func main() {
p := newCounter()
*p++
fmt.Println(*p) // 1
pt := &Point{1, 2}
pt.X = 10 // 구조체 포인터는 (*pt).X 대신 pt.X 로 쓴다
}Go의 인자 전달은 언제나 값 복사입니다. 큰 구조체를 넘기거나 함수 안에서 원본을 바꿔야 할 때 포인터를 씁니다. 슬라이스와 맵은 내부에 참조를 담고 있어서 복사해도 같은 데이터를 가리킵니다.
range는 rune 단위로 돕니다.append의 결과는 다시 받아야 합니다.v, ok := m[k]로 존재 여부를 확인합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.