그래 그리 쉽지는 않겠지

검색

검색 아이콘검색을 여는 아이콘

Tucker의 Go 언어 프로그래밍 Ch12 ~ Ch17

2023-10-14

이 글은 골든래빗 《 Tucker의 Go언어 프로그래밍 》의 12장~17장 써머리입니다.

Ch12 ~ Ch17

  • 배열
  • 구조체
  • 포인터
  • 문자열
  • 패키지
  • 숫자 맞추기 게임 만들기

# 배열

타입이 같은 값 여러 개를 가지는 자료구조

1
2
3
4
var 변수명 [요소 개수]타입

// 요소 개수를 생략하여 초기화
x := [...]int{10, 20, 30}

배열 선언 시 개수는 항상 상수

range 순회

1
2
3
for i, v := range t { // i: 인덱스, v: 원솟값
	...
}

연속된 메모리

  • 요소 위치
    • 배열 시작 주소 + (인덱스 x 타입 크기)

배열 복사

대입 연산자: 우변의 값을 좌변의 메모리 공간에 복사

  • 타입의 크기만큼 복사 (타입이 같아야 함)

# 구조체

다른 타입의 여러 필드를 묶어서 사용하는 타입

1
2
3
4
type 타입명 struct {
	필드명 타입
	...
}

응집도 증가

  • 관련 데이터들을 묶음
  • 재사용성을 증가시킴

구조체 초기화

  • 초깃값 생략
    • 모든 필드는 기본값(zero value)로 초기화 됨
  • 일부 필드 초기화
    • 나머지 필드는 기본값으로 초기화 됨

구조체 값 복사

구조체의 모든 필드값이 복사

# 구조체를 포함하는 구조체

일반 타입처럼 다른 구조체를 필드로 포함할 수 있음

필드명 생략 (포함된 필드)

  • 상위 구조체 변수를 통해 하위 구조체(구조체 필드)에 바로 접근 가능
  • 상위 구조체와 하위 구조체의 필드명이 겹칠 때
    • 상위 구조체의 필드 사용
    • 하위 구조체의 필드는 하위 구조체명을 사용하여 접근

# 메모리 패딩

레지스터 크기

한 번 연산에 사용할 수 있는 크기

  • 32비트 컴퓨터: 4바이트
  • 64비트 컴퓨터: 8바이트

메모리 정렬을 위해 필드 사이에 공간을 띄우는 것

  • 변수의 바이트 수와 시작 주소를 배수로 맞춤 (e.g. 4바이트 변수: 4의 배수인 시작 주소)
  • 64비트 컴퓨터에서 8바이트 타입의 변수
    • 레지스터 크기가 8바이트이므로 8의 배수 주소에 할당되어 있지 않으면 성능상 손해를 봄

# 포인터

메모리 주소를 값으로 갖는 타입

  • 메모리 주소
    • 일반 변수 앞에 & 를 붙여 가져올 수 있음

# 포인터 변수

메모리 주소값을 변수값으로 가질 수 있는 변수

  • 64비트 컴퓨터 기준 8바이트 크기를 가짐

선언

1
var p *int

접근

포인터 변수 앞에 * 를 붙여 접근

1
*p = 20

nil

어떤 메모리 공간도 가리키고 있지 않음을 나타냄

new() 내장 함수

타입을 메모리에 할당하고 기본값으로 채운 뒤 그 주소를 반환

# 탈출 분석

메모리 공간이 함수 외부로 공개되는지 여부를 검사하여 메모리를 할당

함수 외부로 공개(메모리 주소를 반환)되는 인스턴스

  • 함수가 종료되어도 사라지지 않음
  • 스택이 아닌 힙 메모리에 할당

# 문자열

문자의 집합

표현

  • 큰따옴표 ("): 특수 문자 동작
  • 백쿼트 ( ` ): 특수 문자도 일반 문자 처리
    • 여러 줄에 걸쳐서 문자열을 쓸 수 있음

rune 타입

  • 문자 하나를 표현
  • int32 타입(4바이트)의 별칭 타입
  • %c 포맷을 사용하여 문자 하나를 출력 가능

len()

  • 문자열
    • 문자열이 차지하는 메모리 크기(바이트)를 반환
  • 슬라이스
    • 배열의 요소 개수를 반환

# 타입 변환

  • []rune
    • 각 글자들로 이뤄진 배열
  • []byte
    • 1바이트 배열

# 문자열 순회

  • 바이트 단위 순회: 인덱스 사용
    1
    2
    3
    
    for i := 0; i < len(str); i++ {
      str[i]
    }
    
  • 한 글자 단위 순회
    • []rune 타입 사용
      1
      2
      3
      4
      
      arr := []rune(str)
      for i := 0; i < len(arr); i++ {
          arr[i]
      }
      
    • range 키워드 사용
      1
      2
      3
      
      for _, v := range str {
          v // rune 타입
      }
      

# string 구조

  • Data: uintptr (8바이트)
    • 문자열의 메모리 주소를 나타내는 포인터
  • Len: int (8바이트)
    • 문자열의 길이

# 불변성

포인터를 통해 문자열을 가리키기 때문에 불변성이 없다면 추적이 어려움

  • 전체 변경은 가능
    • Data와 Len 값이 변경
  • 인덱스를 통해 일부만 변경은 불가능
  • 새로운 메모리 공간에 문자열을 복사
    • 슬라이스로 타입 변환 시
    • 문자열 합산 시
      • 빈번하다면 strings.builder 사용을 권장

# 패키지

코드를 묶는 가장 큰 단위

main 패키지

  • main() 함수 (프로그램 시작점) 을 포함한 패키지

# 패키지 설치

  • go 언어에서 기본 제공하는 패키지
    • go 설치 경로에서 찾음
  • 외부 저장소에 저장된 패키지
    • 외부 저장소에서 go 모듈에 정의된 버전에 맞게 다운로드
  • 현재 모듈 아래 위치한 패키지
    • 현재 폴더 아래 있는 패키지를 찾음

# 패키지 초기화

  1. 패키지 임포트
  2. 패키지 내 전역 변수 초기화
  3. 패키지 내 init() 함수 호출
    • init() 함수 조건: 입력 매개변수가 없고 반환값도 없는 함수

# 숫자 맞추기 게임 만들기

프로그램에서 임의로 선정한 숫자 맞추기

  1. 프로그램 실행
  2. 콘솔에 예상 숫자 입력
  3. 정답이면 프로그램 종료
  4. 더 작거나, 더 크면 해당 메시지 출력
  5. 메시지를 보고 2번부터 반복
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package main  
  
import (  
    "bufio"  
    "errors"
    "fmt"
    "os"
    "sync"
    "time"
  
    "math/rand"
)
  
const (  
    smaller = iota + 1  
    larger
	equal

    inputPrint   = "숫자값을 입력하세요"  
  
    smallerPrint = "입력하신 숫자가 더 작습니다."  
    largerPrint  = "입력하신 숫자가 더 큽니다."  
    equalPrint   = "숫자를 맞췄습니다. 축하합니다."  
    countPrint   = "시도한 횟수"  
)  
  
var wg sync.WaitGroup  
  
func main() {  
    resultCh := make(chan game)  
  
    go func() {  
       defer close(resultCh)  
       err := newGame(resultCh)  
       if err != nil {  
          panic(err)  
       }  
    }()  
  
    printResult(resultCh)  
}  
  
type game struct {  
    num    int   // 생성된 숫자  
    result uint8 // 게임 결과  
    cnt    int   // 시도한 횟수  
}  
  
// newGame 게임을 생성하고 실행  
func newGame(resultCh chan game) error {  
    g := &game{  
       num: newNum(),  
    }  
    stdin := bufio.NewReader(os.Stdin)  
    for {  
       wg.Wait() // 출력이 나올때까지 대기  
       if err := playGame(stdin, g); err != nil {  
          return err  
       }  
       resultCh <- *g  
       wg.Add(1)  
       if g.result == equal {  
          break  
       }  
    }  
    return nil  
}  
  
// printResult 게임의 결과를 출력  
func printResult(gameCh chan game) {  
    for {  
       g, ok := <-gameCh  
       if !ok {  
          break  
       }  
       switch g.result {  
       case smaller:  
          fmt.Println(smallerPrint)  
       case larger:  
          fmt.Println(largerPrint)  
       case equal:  
          fmt.Printf("%s %s: %d", equalPrint, countPrint, g.cnt)  
       }  
       wg.Done()  
    }  
}  
  
// playGame 사용자 입력을 받아 결과를 확인  
func playGame(stdin *bufio.Reader, g *game) error {  
    fmt.Printf("%s:", inputPrint)  
    num, err := inputNum(stdin)  
    if err != nil {  
       return err  
    }  
    g.cnt = g.cnt + 1  
    g.result = g.checkNum(num)  
    return nil  
}  
  
// checkNum 게임의 숫자와 입력된 숫자를 비교  
func (g *game) checkNum(num int) (result uint8) {  
    switch {  
    case g.num > num:  
       result = smaller  
    case g.num < num:  
       result = larger  
    case g.num == num:  
       result = equal  
    }  
    return  
}  
  
// newNum 랜덤한 숫자 생성  
func newNum() int {  
    // 다양한 시퀀스를 생성하기 위해선, 변화하는 시드값을 주어야 함  
    s := rand.NewSource(time.Now().UnixNano())  
    r := rand.New(s)  
    return r.Intn(100)  
}  
  
// inputNum 사용자 숫자 입력  
func inputNum(stdin *bufio.Reader) (int, error) {  
    var n int  
    _, err := fmt.Scanln(&n)  
    if err != nil {  
       _, _ = stdin.ReadString('\n') // 입력 스트림 비움  
       return 0, errors.New("scan error")  
    }  
    return n, nil  
}

# References