-
4장 복합 타입프로그래밍/Golang 2019. 10. 30. 21:25
The Go Programming Language
4장 복합 타입
복합 타입은 기본 타입을 다양한 방법으로 결합해 생성한다.
복합 타입은 배열, 슬라이스, 맵, 구조체가 있다.
배열과 구조체는 집합 타입이다. 이 값들은 메모리에 있는 다른 값들을 연결한 것이다. 배열은 동종(homogeneous(원소가 모두 같은 타입))인 반면, 구조체는 이종(heterogeneous)이다.
배열과 구조체는 모두 고정된 크기다. 반면에 슬라이스와 맵은 동적 데이터 구조이며, 값을 추가할 때마다 커진다.
4.1 배열
배열은 0개 이상의 특정 타입 원소로 이뤄진 고정 길이 시퀀스다.
배열의 개별 원소는 기존 첨자 표기법으로 접근한다.
첨자의 범위는 0부터 배열 길이-1까지다.
내장 함수 len()은 배열의 원소 수를 반환한다.
var a [3]int // array of 3 integers fmt.Println(a[0]) // print the first element fmt.Println(a[len(a)-1]) // print the last element, a[2] // Print the indices and elements. for i, v := range a { fmt.Printf("%d %d\n", i, v) } // Print the elements only. for _, v := range a { fmt.Printf("%d\n", v) }
- 초기화
원소 타입의 제로 값으로 설정
숫자의 기본 값은 0
배열 리터럴을 통해 배열을 값의 목록으로 초기화 가능
var q [3]int = [3]int{1, 2, 3} var r [3]int = [3]int{1, 2} fmt.Println(r[2]) // "0"
배열 리터럴에서 길이 부분을 "..."로 사용하면 배열의 길이는 초기화 값의 갯수로 결정
q := [...]int{1, 2, 3} fmt.Printf("%T\n", q) // "[3]int"
리터럴 문법은 배열, 슬라이스, 맵, 구조체 모두 유사하다.
- 배열의 크기
해당 타입의 일부이므로 [3]int와 [4]int는 서로 다른 타입
크기는 프로그램을 컴파일할 때 계산할 수 있는 값을 표현하는 상수 표현식
q := [3]int{1, 2, 3} q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
- 형태
위는 순서가 정해진 값의 목록 형태이지만 다음은 인덱스와 값 쌍의 목록을 지정 가능
const ( USD Currency = iota EUR GBP RMB ) symbol := [...]string{USD: "$", EUR: "9", GBP: "!", RMB: """} fmt.Println(RMB, symbol[RMB]) // "3 ""
위 형태에서 인덱스는 순서에 상관없이 나타날 수 있으며 일부를 생략 가능
지정되지 않은 값은 해당 원소 타입의 제로 값
r := [...]int{3: -1} // 마지막 요소 값만 -1, 그 외 모두 0 fmt.Println(r[0], r[1], r[2], r[3]) // "0 0 0 -1"
- 비교
배열 원소를 비교할 수 있으면 배열 타입도 비교 가능
a := [2]int{1, 2} b := [...]int{1, 2} c := [2]int{1, 3} fmt.Println(a == b, a == c, b == c) // "true false false" d := [3]int{1, 2} fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
crypto/sha256 패키지의 Sum256 함수는 임의의 바이트 슬라이스에 저장된 메시지의 SHA256 암호화 해시나 다이제스트(digest, 축약)를 생성하고 다이제스트는 256비트이므로 타입이 [32]byte이며 두 다이제스트가 동일하면 두 메시지가 동일할 가능성이 높음
gopl.io/ch4/sha256 import "crypto/sha256" func main() { c1 := sha256.Sum256([]byte("x")) c2 := sha256.Sum256([]byte("X")) fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) // Output: // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 // false // [32]uint8 }
함수의 인자로 배열이 들어오면 복사본을 전달(다른 언어와 다름)
참조로 전달하는 방법은 명시적으로 배열의 포인터를 전달// [32]byte 배열의 내용을 0으로 만든다. func zero(ptr *[32]byte) { for i := range ptr { ptr[i] = 0 } }
배열은 고정 크기라는 점 때문에 유연하지 못하다. 대신 슬라이스를 사용한다.
4.2 슬라이스
모든 원소가 같은 타입인 가변 길이 시퀀스
원소가 T 타입일 때 []T로 표현(크기가 없는 배열 타입처럼 보임)
- 구조
배열과 슬라이스는 밀접하게 연결되어 있음
슬라이스는 "슬라이스의 내부 배열"이라고 알려진 배열의 원소들 일부(또는 전체)에 접근할 수 있는 경량 자료 구조
슬라이스는 포인터, 길이, 용량으로 구성됨
포인터는 슬라이스로 접근할 수 있는 배열의 첫 번째 원소를 가리킴(반드시 배열의 첫 번째 원소일 필요는 없음)
길이는 슬라이스 원소의 갯수(len() 함수 사용)
길이는 용량을 초과할 수 없음
용량은 보통 슬라이스 내부 배열의 시작과 끝 사이에 있는 원소의 갯수(cap() 함수 사용)
여러 슬라이스가 같은 내부 배열을 공유 가능
배열의 일부를 중복 참조 가능
0번째 인덱스는 빈 문자열(월은 1부터 시작)
Q2 := months[4:7] summer := months[6:9] fmt.Println(Q2) // ["April" "May" "June"] fmt.Println(summer) // ["June" "July" "August"]
- 참조
슬라이스에는 배열 원소에 대한 포인터가 있으므로 슬라이스를 함수에 전달하면 함수가 내부 배열 원소를 변경
gopl.io/ch4/rev func main { //!+array a := [...]int{0, 1, 2, 3, 4, 5} reverse(a[:]) fmt.Println(a) // "[5 4 3 2 1 0]" //!-array //!+slice s := []int{0, 1, 2, 3, 4, 5} // Rotate s left by two positions. reverse(s[:2]) reverse(s[2:]) reverse(s) fmt.Println(s) // "[2 3 4 5 0 1]" //!-slice //array reverse1(a) } // reverse reverses a slice of ints in place. func reverse(s []int) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } func reverse1(s [6]int) { ... }
- 초기화
슬라이스를 초기화하는 표현식은 배열를 초기화하는 표현식과 다름
슬라이스 리터럴은 배열 리터럴처럼 보이고, 값의 시퀀스가 콤마로 구별되며 중괄호로 둘러싸여 있지만 크기를 지정하지 않음
슬라이스 리터럴은 배열 리터럴과 마찬가지로 순서대로 값을 지정하거나, 명시적으로 인덱스를 할당하거나, 두 가지 스타일을 혼합해 사용
- 비교
배열과 달리 비교할 수 없음
표준 라이브러리 중 최적화된 bytes.Equal 함수로 두 바이트 슬라이스([]byte)를 비교할 수 있지만 다른 타입의 슬라이스는 직접 비교 연산을 수행해야 함
func equal(x, y []string) bool { if len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true }
깊은 동등성 비교의 문제점
첫번째: 슬라이스 원소는 배열 원소와는 달리 간접 참조이므로 슬라이스는 내부에 자기 자신을 포함할 수 있고 이러한 경우에 대처하는 방법이 있긴 하지만 그중 어느 것도 간단하지도 효율적이지도 않으며 무엇보다 정확하지 않음
두번째: 슬라이스 원소는 간접 참조이므로 고정된 슬라이스의 값은 내부 배열이 변경됨에 따라 비교 시 다른 값을 가질 수 있음
맵 타입과 같은 해시 테이블은 키의 얕은 사본(swallow copy)만을 만들기 때문에 각 키의 동등성이 해시 테이블의 수명 기간 동안 동일하게 유지돼야 하며 따라서 깊은 동등성이 없는 슬라이스는 맵 키로 적합하지 않음
포인터나 채널과 같은 참조 타입에서는 == 연산자로 참조되는 객체의 동등성, 즉 두 개체가 같은 것을 가리키는 지를 테스트하며 이와 유사한 슬라이스의 "얕은" 동등성 테스트가 유용할 수 있고 이를 통해 맵에서의 문제도 해결할 수 있겠지만 슬라이스와 배열에서 == 연산자의 일관성 없는 취급은 혼동을 유발하며 가장 안전한 선택은 슬라이스의 모든 비교 연산을 막는 것임
슬라이스에는 nil과의 비교만이 유일하게 허용
if summer == nil { /* ... */ }
슬라이스 타입의 제로 값은 nil
빈 슬라이스에는 내부 배열이 없음
빈 슬라이스의 길이와 용량은 0이지만 []int{}나 make([]int, 3)[3:]과 같이 비어있지 않다면 길이와 용량이 0인 슬라이스도 있음
특정 슬라이스 타입의 빈 값은 []int(nil)과 같은 변환 표현식 가능
var s []int // len(s) == 0, s == nil s = nil // len(s) == 0, s == nil s = []int(nil) // len(s) == 0, s == nil s = []int{} // len(s) == 0, s != nil
슬라이스가 비어 있는지 확인은 s == nil이 아니라 len(s) == 0을 사용
빈 슬라이스는 nil과 비교할 때 외에는 길이가 0인 슬라이스처럼 동작
- 내장함수 make()
지정된 원소 타입, 길이, 용량의 슬라이스를 생성
용량 인자는 용량의 길이와 같은 경우 생략 가능
make([]T, len) make([]T, len, cap) // same as make([]T, cap)[:len]
make()는 내부적으로 이름 없는 배열 변수를 만들고 이 배열의 슬라이스를 반환
이 배열은 반환된 슬라이스를 통해서만 접근 가능
첫 번째 형태의 슬라이스는 전체 배열의 참조
두 번째 형태의 슬라이스는 배열의 처음 len개의 원소에 대한 참조이지만 용량은 전체 배열을 포함되고 추가 원소는 향후 커질 때에 대한 대비임
4.2.1 append 함수
내장된 append 함수는 슬라이스에 항목을 추가
var runes []rune for _, r := range "Hello, BF" { runes = append(runes, r) } fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' 'B' 'F']"
위 루프는 append를 통해 문자열 리터럴로 인코딩된 9개 룬의 슬라이스를 생성
[]rune("Hello, BF") 변환을 이용하면 더 편리함
append 함수는 슬라이스 동작 원리를 이해하는 데 매우 중요
gopl.io/ch4/append func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { // There is room to grow. Extend the slice. z = x[:zlen] } else { // There is insufficient space. Allocate a new array. // Grow by doubling, for amortized linear complexity. zcap := zlen if zcap < 2*len(x) { zcap = 2 * len(x) } z = make([]int, zlen, zcap) copy(z, x) // a built-in function; see text } z[len(x)] = y return z }
- appendInt 함수
호출될 때마다 슬라이스의 기존 배열에 새 원소를 담을 용량이 충분한지 확인
충분하다면 (원래 배열 안에서) 더 큰 슬라이스를 정의해 슬라이스를 확장하고 원소 y를 새 공간에 복사한 후 이 슬라이스를 반환
입력 x와 결과 z는 동일한 내부 배열을 공유
확장할 공간이 부족하면 appendInt는 결과를 담기에 충분한 새 배열을 할당한 후 x에서 값을 복사하고 새 원소 y를 추가
이때 결과 z는 x가 참조하고 있던 내부 배열과는 다름
명시적인 루프로 원소를 복사하는 것이 더 직관적이지만, 같은 타입의 슬라이스 사이에 원소를 복사할 때에는 내장된 함수 copy를 사용하는 것이 더 쉬움
copy의 첫 번째 인수는 대상이고, 두 번째 인수는 원본이며, 이는 dst = src 할당문에서 피연산자의 순서와 동일
- 내장된 append 함수
appendInt의 단순한 방식 대신 더 정교한 확장 방식을 사용하며 보통 append 호출로 재할당이 일어나는지 여부는 알 수 없기 때문에 원본 슬라이스와 결과 슬라이스가 같은 배열을 참조한다고 가정할 수 없으며 다른 배열이라고 가정할 수도 없음
마찬가지로 이전 슬라이스 원소에 대한 할당이 새 슬라이스에 반연된다고(또는 반영된지 않는다고) 가정해서는 안 됨
그렇기 때문에 일반적으로 append 호출의 결과를 append로 전달된 슬라이스 변수에 할당
runes = append(runes, r)
append 호출뿐만 아니라 슬라이스의 길이나 용량을 변경하는 함수, 또는 슬라이스가 다른 내부 배열을 참조하게 하는 함수도 슬라이스 변수를 갱신해야 함
슬라이스를 올바르게 사용하려면 슬라이스 내부 배열의 원소는 간접적으로 참조하지만 슬라이스의 포인터, 길이, 용량은 그렇지 않다는 점을 명심해야 함
슬라이스는 "순수한" 참조 타입이 아니라 다음 구조체와 같은 집합형 타입에 가까움
type IntSlice struct { ptr *int len, cap int }
appendInt 함수는 하나의 원소를 슬라이스에 추가하지만 내장된 append는 하나 이상의 새 원소를 추가하거나 원소의 전체 슬라이스도 추가 가능
var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) // append the slice x fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
appendInt도 내장된 append와 동일하게 동작하게 할 수 있음
appendInt 선언의 말줄임표 "..."는 함수를 가변형(variadic)으로 만들고 개수 제한이 없음
위 append 호출에서 말줄임표는 슬라이스로 인자 목록을 전달하는 방법을 보여줌
func appendInt(x []int, y ...int) []int { var z []int zlen := len(x) + len(y) // ...expand z to at least zlen... copy(z[len(x):], y) return z }
4.2.2 슬라이스 직접 변경 기법
- 방법1
gopl.io/ch4/nonempty // Nonempty is an example of an in-place slice algorithm. package main import "fmt" // nonempty returns a slice holding only the non-empty strings. // The underlying array is modified during the call. func nonempty(strings []string) []string { i := 0 for _, if s := range strings { s != "" { strings[i] = s i++ } } return strings[:i] }
입력과 출력 슬라이스가 같은 기본 배열을 공유하는 부분은 알아보기 어려움
data := []string{"one", "", "three"} fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` fmt.Printf("%q\n", data) // `["one" "three" "three"]`
두 번째 출력 구문에서 볼 수 있듯이 data의 일부는 덮어써지지만 새 배열을 할당하지 않아도 됨
따라서 보통 data = nonempty(data)처럼 작성
- 방법2
func nonempty2(strings []string) []string { out := strings[:0] // zero-length slice of original for _, s := range strings { if s != "" { out = append(out, s) } } return out }
배열을 재사용하려면 어떻게 변형하더라도 각 입력 값에 대해 최대 한 개의 출력 값이 만들어지므로 원소 목록에서 일부를 제외하거나 인접한 원소를 결합하는 여러 알고리즘에서 활용 가능
복잡한 슬라이스의 사용법은 규칙이 아니라 예외에 가깝지만 때로는 이 방식이 더 명확하고 효율적이며 유용함
- 스택 예
stack = append(stack, v) // push v top := stack[len(stack)-1] // top of stack stack = stack[:len(stack)-1] // pop
- 슬라이스 중간 원소 제거
남아있는 원소의 순서를 우지하려면 copy를 사용해 상위 원소를 한 단계씩 아래로 밀어내어 빈 공간을 채움
func remove(slice []int, i int) []int { copy(slice[i:], slice[i+1:]) return slice[:len(slice)-1] } func main() { s := []int{5, 6, 7, 8, 9} fmt.Println(remove(s, 2)) // "[5 6 8 9]" }
순서를 유지할 필요가 없다면 마지막 원소를 빈 공간으로 옮기면 됨
func remove(slice []int, i int) []int { slice[i] = slice[len(slice)-1] return slice[:len(slice)-1] } func main() { s := []int{5, 6, 7, 8, 9} fmt.Println(remove(s, 2)) // "[5 6 9 8] }
4.3 맵
- 해시 테이블
모든 데이터 구조 중 가장 기발함
순서 없는 키/값 쌍의 모음
모든 키는 별개이며 주어진 키와 관련된 값은 해시 테이블의 크기와 무관하게 평균적으로 일정한 회수의 키 비교를 통해 추출, 갱신, 제거 가능
- 맵
맵은 해시 테이블의 참조
맵 타입은 K와 V가 각각 키와 값의 타입일 때 map[K]V 정의
주어진 맵에서 모든 키는 동일한 타입이고 모든 값도 동일한 타입지만 키와 값의 타입은 서로 다름
키가 맵 안에 있는 키와 같은지 테스트해야 하므로 키 타입 K는 ==로 비교 가능
부동소수점 수 비교는 할 수 있지만 NaN을 가질 수 있으므로 동일성 비교 목적으로 사용하기에는 좋지 않음
갑 타입 V에는 제약이 없음
내장 함수 make로 맵 생성 가능
ages := make(map[string]int) // mapping from strings to ints
맵 리터럴로 초기 키/값을 갖는 새 맵 생성 가능
ages := map[string]int{ "alice": 31, "charlie": 34, } 또는, ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34
새 빈 맵
map[string]int{}
맵 원소는 첨자 표기법으로 접근
ages["alice"] = 32 fmt.Println(ages["alice"]) // "32"
내장 함수 delete로 삭제
delete(ages, "alice") // remove element ages["alice"]
위 모든 작업은 원소가 맵에 없어도 안전하게 수행됨
맵 안에 없는 키로 맵을 조회하면 해당 타입의 제로 값을 반환
"bob"이 아직 맵 안의 키가 아니더라도 ages["bob"]의 값은 0이므로 다음 코드는 작동함
ages["bob"] = ages["bob"] + 1 // happy birthday!
맵 원소에는 짧은 할당 형태인 x += y와 x++ 가능하고 위 문장은 다음과 같음
ages["bob"] += 1
위 문장을 더 간결하게 하면
ages["bob"]++
맵 원소는 변수가 아니므로 주소를 얻을 수 없음
_ = &ages["bob"] // compile error: cannot take address of map element
맵 원소의 주소를 얻을 수 없는 이유 중 하나는 맵이 커지면 기존 원소에 재해시(rehash)가 일어나면서 새 저장 공간으로 옮겨질 수 있으므로 잠재적으로 주소가 무효화될 수 있기 때문
맵의 모든 키/값 쌍을 열거하려면
for name, age := range ages { fmt.Printf("%s\t%d\n", name, age) }
- 맵 반복의 순서
정해져 있지 않으며 구현물별로 다른 해시 함수를 사용해 순서가 달라질 수 있음
실제로 순서는 임의이며, 실행시마다 달라지고 이는 의도적인 구현
순서를 매번 다르게 하면 프로그램이 구현에 상관없이 안정적으로 만들어지게 도와줌
키/값 쌍을 순서대로 열거하려면 키를 명시적으로 정렬해야 함
예를 들어 키가 문자열이라면 sort 패키지의 Strings 함수를 사용
import "sort" var names []string for name := range ages { //값 변수 생략 names = append(names, name) } sort.Strings(names) for _, name := range names { // 빈 식별자 _를 사용해 인덱스 무시 fmt.Printf("%s\t%d\n", name, ages[name]) }
- nil
맵 타입의 제로 값은 nil이며 해시 테이블에 대한 참조가 없음
var ages map[string]int fmt.Println(ages == nil) // "true" fmt.Println(len(ages) == 0) // "true"
맵에 대한 조회, delete, len, range 루프 등의 대부분 작업에 nil 맵을 사용해도 안전하며 이는 nil 맵이 빈 맵처럼 동작하기 때문
nil 맵에 저장하면 패닉 발생
ages["carol"] = 21 // panic: assignment to entry in nil map
- 첨자
맵 원소를 참자로 잡근하면 항상 값이 산출
맵 안에 키가 있으면 키에 해당하는 값을 얻음
아니면 해당 원소의 제로 값을 얻음
간혹 원소가 있는지 여부를 알 필요가 있음(없는 원소와 0인 원소 구별)
age, ok := ages["bob"] if !ok { /* "bob" is not a key in this map; age == 0. */ }
위 구문은 다음과 같이 결합해 많이 사용
if age, ok := ages["bob"]; !ok { /* ... */ }
맵에 첨자로 접근하면 두 값이 나옴
두 번째는 불리언으로 원소의 유무 여부
- 비교
맵은 서로 비교할 수 없음
nil과 비교 가능
두 맵이 같은 키와 같은 연관된 값을 갖는지 확인하려면 루프를 작성해야 함
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true }
GO에는 set 타입이 없지만 맵 키는 유일하므로 맵을 이 목적으로 사용
- 도우미 함수
키가 슬라이스인 맵이나 집합이 필요하지만 맵의 키는 비교할 수 있어야 하므로 직접 이렇게 쓸 수 없음
두 단계를 거치면 가능
x와 y가 같다고 간주할 때에만 각 키를 문자열 k(x) == k(y)로 매핑하는 도우미 함수 k를 정의
키가 문자열인 맵을 생성하고 이 맵을 사용하기 전에 각 키에 도우미 함수를 적용
- 도우미 함수 예
맵을 사용해 Add가 주어진 문자열 목록이 호출된 횟수를 기록
fmt.Sprintf로 문자열 슬라이스를 맵 키에 적합한 단일 문자열로 변환하고 %q를 이용해 각 슬라이스 원소의 경계를 인용 부호로 구분
var m = make(map[string]int) func k(list []string) string { return fmt.Sprintf("%q", list) } func Add(list []string) { m[k(list)]++ } func Count(list []string) int { return m[k(list)] }
슬라이스 외에 다른 비교할 수 없는 타입에도 이 방법을 사용 가능
비교할 수 있는 키 타입에서도 대소문자 구별 없이 문자열을 비교하는 등, ==가 아닌 동등성을 정의할 때 유용
k(x)의 타입이 문자열일 필요는 없음
정수, 배열, 구조체 등 필요한 비교 속성을 가지면 비교할 수 있는 키 타입이면 됨
- 맵의 값 타입도 맵이나 슬라이스와 같은 복합 타입
다음 코드에서 graph의 키 타입은 string이며, 값 타입은 문자열 집합을 나타내는 map[string]bool임
gopl.io/ch4/graph var graph = make(map[string]map[string]bool) func addEdge(from, to string) { edges := graph[from] if edges == nil { edges = make(map[string]bool) graph[from] = edges } edges[to] = true } func hasEdge(from, to string) bool { return graph[from][to] }
addEdge 함수는 키가 처음 나타날 때 해당 값을 초기화해 맵을 채우는 관용적인 방법
hasEdge 함수는 맵 원소가 없을 때 제로 값을 어떻게 사용하는지 보임
from이나 to가 없을 때에도 graph[from][to]는 항상 의미가 있는 결과를 반환
4.4 구조체
0개 이상의 명명된 임의의 타입 값을 하나의 개체로 모으는 집합형 데이터 타입
각 값은 필드(field)라 함
- 예
//구조체 type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int } //인스턴스 변수 var dilbert Employee //필드접근 dilbert.Salary -= 5000 //주소를 얻어 포인터로 접근 position := &dilbert.Position *position = "Senior " + *position //점 표기법은 구조체 포인터에서 가능 var employeeOfTheMonth *Employee = &dilbert employeeOfTheMonth.Position += " (proactive team player)" //= *employeeOfTheMonth).Position += " (proactive team player)"
- 구별
필드 순서는 타입을 구별
type Employee struct { ID int Name, Address string DoB Position Salary ManagerID int time.Time string int }
Position 필드의 선언을 결합하거나 Name과 Address의 순서를 바꾸면 다른 구조체 타입임
- 익스포트(export)
구조체 필드의 이름이 대문자로 시작하면 익스포트됨
구조체 타입은 익스포트된 필드와 익스포트되지 않은 필드를 모두 가질 수 있음
- 명명된 구조체 타입
명명된 구조체 타입 S에는 동일 타입인 S 필드를 선언할 수 없음
집합형 값은 자기 자신을 포함할 수 없음
하지만 S는 포인터 타입인 *S 필드를 선언할 수 있으므로 링크드 리스트나 트리 등의 재귀형 데이터 구조를 만들 수 있음
gopl.io/ch4/treesort type tree struct { value int left, right *tree } // Sort sorts values in place. func Sort(values []int) { var root *tree for _, v := range values { root = add(root, v) } appendValues(values[:0], root) } // appendValues appends the elements of t to values in order // and returns the resulting slice. func appendValues(values []int, t *tree) []int { if t != nil { values = appendValues(values, t.left) values = append(values, t.value) values = appendValues(values, t.right) } return values } func add(t *tree, value int) *tree { if t == nil { // Equivalent to return &tree{value: value}. t = new(tree) t.value = value return t } if value < t.value { t.left = add(t.left, value) } else { t.right = add(t.right, value) } return t }
- 제로 값
구조체의 제로 값은 각 필드의 제로 값으로 구성
bytes.Buffer에서 구조체의 초기 값은 즉시 사용 가능한 빈 버퍼
sync.Mutex의 제로 값은 즉시 사용 가능한 잠금 해제 상태의 뮤텍스(unlocked mutex)
- 빈 구조체
필드가 없는 구조체 타입을 빈 구조체라 하며, strunct{}로 작성
크기가 0이고 아무런 정보가 없음
4.4.1 구조체 리터럴
구조체 타입의 값은 해당 필드의 값을 지정하는 구조체 리터럴로 쓸 수 있음
type Point struct{ X, Y int } p := Point{1, 2}
- 형태
구조체 리터럴에는 2가지 형태가 있음
위 코드에서 첫 번째 형태는 모든 필드의 값을 올바른 순서로 지정해야 함
다음은 구조체 값들이 필드의 일부 또는 전부가 연관되는 값 목록으로 초기화(이 방법을 많이 사용)
anim := gif.GIF{LoopCount: nframes}
필드가 생략되면 해당 타입의 제로 값으로 설정됨
이름이 있으므로 필드의 순서는 무관함
한 리터럴에서 2가지 형태를 섞어서 쓸 수 없음
익스포트되지 않은 식별자는 다른 패키지에서 참조할 수 없다는 규칙으로 인해 (순서 기준으로) 첫 번째 형태에서는 미리보기를 할 수 없음
package p type T struct{ a, b int } // a and b are not exported package q import "p" var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b var _ = p.T{1, 2} // compile error: can't reference a, b
- 전달 및 반환
구조체 값은 함수 인자로 전달하거나 함수에서 반환할 수 있음
func Scale(p Point, factor int) Point { return Point{p.X * factor, p.Y * factor} } fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
큰 구조체 유형은 효율성을 위해 보통 포인터를 이용해서 간접적으로 함수로 전달하거나 함수에서 반환
func Bonus(e *Employee, percent int) int { return e.Salary * percent / 100 }
함수가 인자를 변경해야 할 경우 포인터를 사용
func AwardAnnualRaise(e *Employee) { e.Salary = e.Salary * 105 / 100 }
구조체는 포인터를 통해 처리되기 때문에 다음과 같이 사용
pp := &Point{1, 2}
&Point{1, 2}는 함수 호출과 같은 표현식 내에서 직접 사용할 수 없음
위는 다음과 같음
pp := new(Point) *pp = Point{1, 2}
4.4.2 구조체 비교
구조체의 모든 필드가 비교 가능하다면 구조체 자체도 비교 가능하므로 해당 타입의 두 표현식은 ==나 !=로 비교 가능
== 연산은 두 구조체의 대응하는 필드를 순서대로 비교
비교 가능한 구조체 타입은 다른 비교할 수 있는 타입과 마찬가지로 맵의 키로 사용 가능
type address struct { hostname string port int } hits := make(map[address]int) hits[address{"golang.org", 443}]++
4.4.3 구조체 내장 필드와 익명 필드
x.d.e.f와 표기법을 x.f와 같은 단축 문법이 있음
명명된 구조체 타입을 다른 구조체 타입의 익명 필드로 사용하면 단축 문법 가능
- 예
type Point struct { X, Y int } type Circle struct { Center //익명 필드 Radius int } type Wheel struct { Circle //익명 필드 Spokes int } var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20
하위 필드를 선택할 때에는 익명 필드의 일부나 전부를 생략할 수 있음
구조체 리터럴에는 단축 문법이 없음
w = Wheel{8, 8, 5, 20} // compile error: unknown fields w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
구조체 리터럴은 타입 선언의 형태를 따라야 하므로 다음 두 형식 중 하나를 사용해야 하며 두 문장은 서로 같음
gopl.io/ch4/embed w = Wheel{Circle{Point{8, 8}, 5}, 20} w = Wheel{ Circle: Circle{ Point: Point{X: 8, Y: 8}, Radius: 5, }, Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) } fmt.Printf("%#v\n", w) // Output: // Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} w.X = 42 fmt.Printf("%#v\n", w) // Output: // Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
익명 필드에는 묵시적인 이름이 있어서 필드 이름이 충돌하므로 같은 타입의 두 익명 필드를 사용할 수 없음
익스포트되지 않았더라도 단축 문법 가능
w.X = 8 // equivalent to w.circle.point.X = 8
익명 필드가 반드시 구조체 타입일 필요는 없고 명명된 타입이나 명명된 타입의 포인터도 가능
왜 하위 필드가 없는 타입을 내장하는가?
답은 메소드와 관련이 있음
내장 타입의 필드를 선택하는 단축 표기법은 내장 타입의 메소드 선택에도 사용 가능
이로 인해 외부의 구조체 타입은 내장 타입의 필드 외에 메소드도 수집
이는 간단한 객체의 구성으로 복잡한 객체의 동작을 만드는 주된 방법
구성(composition)은 GO에서 객체지향 프로그래밍의 핵심
4.5 JSON
자바스크립트 객체 표기법(JSON)은 구조화된 정보를 보내고 받기 위한 표준 표기법
표준 라이브러리 패키지 encoding/json으로 인코딩과 디코딩을 지원
JSON은 자바스크립트 값(문자열, 숫자, 불리언, 배열, 객체)을 유니코드 텍스트로 인코딩한 것
기본 JSON 타입은 숫자(10진 또는 지수 표기), 불리언(true 또는 false), 문자열이며, 문자열은 유니코드 코드 포인트를 큰따옴표로 묶어 표기
백슬러시로 이스케이프 처리를 하지만 JSON에서 \Uhhhh는 룬이 아닌 UTF-16 코드
기본적인 타입은 JSON 배열과 객체를 이용해 재귀적으로 결합 가능
JSON 배열은 순서가 있는 값의 목록으로 대괄호 안에 쉼표로 구분해 작성
JSON 배열은 GO의 배열과 슬라이스를 인코딩하기 위해 사용
JSON 객체는 문자열에서 값으로 매핑
name:value 쌍의 목록을 쉼표로 구분하고 중괄호로 둘러싼 형태로 작성
JSON 객체는 GO의 맵과 구조체를 인코딩하기 위해 사용
- 예
boolean true number -273.15 string "She said \"Hello, BF\"" array ["gold", "silver", "bronze"] object {"year": 1980, "event": "archery", "medals": ["gold", "silver", "bronze"]}
다음은 영화 리뷰를 수집하고 영화를 추천하는 애플리케이션은 Movie 데이터 타입과 전형적인 값의 목록을 선언(Year와 Color 필드 선언 뒤에 오는 문자열 리터럴은 필드 태그)
gopl.io/ch4/movie type Movie struct { Title string Year int `json:"released"` //필드 태그 Color bool `json:"color,omitempty"` //필드 태그 Actors []string } var movies = []Movie{ {Title: "Casablanca", Year: 1942, Color: false, Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}}, {Title: "Cool Hand Luke", Year: 1967, Color: true, Actors: []string{"Paul Newman"}}, {Title: "Bullitt", Year: 1968, Color: true, Actors: []string{"Steve McQueen", "Jacqueline Bisset"}}, // ... }
movies 같은 GO 데이터 구조를 JSON으로 변환하는 것을 마샬링(marshalling)이라 함
- 마샬링
마샬링은 json.Marshal로 수행
data, err := json.Marshal(movies) if err != nil { log.Fatalf("JSON marshaling failed: %s", err) } fmt.Printf("%s\n", data)
Marshal은 무의미한 공백을 제외한 매우 긴 문자열을 담은 바이트 슬라이스를 생성
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true," Actors":["Steve McQueen","Jacqueline Bisset"]}]
사람이 읽을 때에는 Marshal의 변형인 json.MarshalIndent 사용
data, err := json.MarshalIndent(movies, "", " ") if err != nil { log.Fatalf("JSON marshaling failed: %s", err) } fmt.Printf("%s\n", data)
결과
마샬링에서는 구조체 필드명을 JSON 객체의 필드명으로 사용(12.6 절의 리플렉션을 통해 수행)
익스포트된 필드만 마샬링되고 이 때문에 GO 필드명을 모두 대문자로 시작
- 필드 태그
결과에서 Year 필드명이 released로 변경되고 Color가 coler로 변경되었는데 이는 필드 태그 때문
필드 태그는 메타데이터 문자열로 컴파일 시에 구조체의 필드와 연관됨
필드 태그에는 어떤 리터럴 문자열도 쓸 수 있지만 통상적으로 공백으로 구분된 key:"value" 쌍으로 해석됨
큰따옴표가 있으므로 필드 태그는 보통 원시 문자열 리터럴로 기록함
json 키는 encoding/json 패키지의 동작을 제거하며 다른 encoding/... 패키지도 이 규칙을 따름
json 필드 태그의 첫 번째 부분은 Go 필드에 다른 JSON 이름을 지정함
필드 태그는 종종 TotalCount 같은 Go 필드명을 이상적인 JSON 이름인 total_count로 지정하기 위해 사용
Color의 태그에는 부가적인 omitempty 옵션이 있으며 이는 필드가 제로 값이거나 비어 있으면 출력하지 않게 함
Year int `json:"released"` Color bool `json:"color,omitempty"`
- 언마샬링
마샬링의 역동작인 JSON을 복호화하고 Go의 데이터 구조에 값을 채우는 것은 언마샬링(unmarshaling)이라 함
json.Unmarshal로 수행
다음 코드는 JSON 영화 데이터를 필드가 Title만 있는 구조체의 슬라이스로 언마샬링
JSON 입력에서 어떤 부분을 복호화하고 어떤 부분을 버릴지 선택 가능
var titles []struct{ Title string } if err := json.Unmarshal(data, &titles); err != nil { log.Fatalf("JSON unmarshaling failed: %s", err) } fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
다음은 웹 서비스 예
gopl.io/ch4/github // Package github provides a Go API for the GitHub issue tracker. // See https://developer.github.com/v3/search/#search-issues. package github import "time" const IssuesURL = "https://api.github.com/search/issues" type IssuesSearchResult struct { TotalCount int `json:"total_count"` Items []*Issue } type Issue struct { Number int HTMLURL string `json:"html_url"` Title string State string User *User CreatedAt time.Time `json:"created_at"` Body string // in Markdown format } type User struct { Login string HTMLURL string `json:"html_url"` }
모든 구조체 필드의 이름은 JSON 이름이 대소문자가 아니더라도 대소문자로 지정해야 함
하지만 언마샬링에서 JSON 이름과 Go의 구조체 이름을 연관시킬 때는 대소문자를 구별하지 않으므로 JSON 이름에는 밑줄이 있지만 Go 이름에 밑줄이 없는 경우에만 필드 태그가 필요함
4.6 텍스트와 HTML 템플릿
포맷과 코드를 분리
이러한 작업은 텍스트나 HTML 템플릿의 변수를 값으로 치환하는 text/template과 html/template 패키지로 가능
- 템플릿
템플릿은 중괄호 {{...}}로 묶인 부분을 한 개 이상 포함하는 문자열이나 파일이며 이 부분을 액션(action)이라고 함
대분분의 문자열은 있는 그대로 인쇄되지만 액션은 다른 동작을 유발시킴
각 액션에는 템플릿 언어의 표현식이 있으며 값 출력, 구조체 필드 선택, 함수와 메소드 호출, if-else 구문과 range 루프를 통한 흐름 제어, 다른 템플릿의 인스턴스 생성 등 가능
- 템플릿 문자열 예
gopl.io/ch4/issuesreport const templ = `{{.TotalCount}} issues: {{range.Items}}---------------------------------------- Number: {{.Number}} User: {{.User.Login}} Title: {{.Title | printf "%.64s"}} Age: {{.CreatedAt | daysAgo}} days {{end}}`
위 템플릿은 일치하는 이슈의 수를 추력하고, 그 후 각 이슈의 번호, 사용자, 제목, 날짜 수를 출력
액션 안에서 현재 값을 참조하는 점 표기법이 있으면 점 "."로 사용
최초의 점은 템플릿 파라미터를 참조하며 예제에서는 github.IssuesSearchResult
{{.TotalCount}} 액션은 TotalCount 필드 값을 확장해 일반적인 방법으로 출력
{{range.Items}}와 {{end}} 액션은 루프를 생성하므로 그 사이의 텍스트는 여러 번 확장되고 점은 Items 구조체의 이후 원소들을 연결
액션 내에서 | 표기법은 한 연산의 결과를 다른 연산의 인자로 만들며 이는 유닉스 셸 파이프라인과 유사함
Title의 경우 두 번째 연산은 모든 템플릿에 내장돼 있는 printf 함수로 fmt.Sprintf와 동의어
Age의 두 번째 연산은 다음에 나오는 daysAgo 함수로 time.Since를 사용해 CreatedAt 필드를 흐른 시간으로 변환
func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24) }
CreatedAt 타입은 string이 아닌 time.Time임
타입에 특정한 메소드를 선언해 문자열 포매팅을 제어(2.5절)하는 것과 마찬가지로 타입에는 JSON 마샬링과 언마샬링 동작을 제어하는 메소드도 선언할 수 있음
JSON으로 마샬링된 time.Time 값은 표준 포맷의 문자열임
- 단계
템플릿으로 출력을 만드는 데 두 단계를 거침
먼저 템플릿을 적절한 내부 표현으로 파싱한 후 특정 입력에 적용시킴(파싱은 한 번만 하면 됨)
앞에서 정이된 템플릿 tmpl을 생성하고 파싱함(메소드는 연속해서 호출 가능)
template.New는 템플릿을 생성하고 반환
Funcs는 템플릿 안에서 접근 가능한 함수 목록에 daysAgo를 추가하고 반환
마지막으로 결과에 Parse를 호출
report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ) if err != nil { log.Fatal(err) }
템플릿은 일반적으로 컴파일 시에 고정되므로 템플릿 파싱 실패는 프로그램의 치명적인 버그를 나타냄
도우미 함수 template.Must로 더 편리하게 예외를 처리하는데 이 함수는 템플릿과 오류 객체를 입력받고 오류가 없는지 확인한 후 오류가 있으면 패닉이고 아니면 템플릿을 반환
- html/template 패키지
HTML, 자바스크립트, CSS, URL 등의 문자열을 자동으로 맥락에 맞게 처리하는 기능 제공
HTML을 생성할 때 반복적으로 발생하는 보안 문제인 인젝션 공격(injection attack)을 막을 수 있음
인젝션은 공격자가 이슈 제목과 같은 문자열 값에 악성 코드를 삽입해 템플릿에서 적절하게 처리를 하지 못할 경우 페이지의 제어권을 얻게 하는 공격
다음 템플릿은 이슈 목록을 HTML 테이블로 출력(임포트 구문이 다른 것에 주목)
gopl.io/ch4/issueshtml import "html/template" var issueList = template.Must(template.New("issuelist").Parse(` <h1>{{.TotalCount}} issues</h1> <table> <tr style='text-align: left'> <th>#</th> <th>State</th> <th>User</th> <th>Title</th> </tr> {{range .Items}} <tr> <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td> <td>{{.State}}</td> <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td> <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td> </tr> {{end}} </table> `))
'프로그래밍 > Golang' 카테고리의 다른 글
Golang 관련 도서 다운로드 및 번역 문서 (0) 2019.11.13 7장 인터페이스 (0) 2019.11.06 6장 메소드 (0) 2019.11.04 5장 함수 (0) 2019.11.03 Golang - 함수 (0) 2019.08.06