Study

[스터디] Tucker의 Go 언어 프로그래밍 내용정리 14~17장

mokpolar 2023. 11. 18. 12:11
반응형

14장 포인터

14.1 포인터란?

  • 포인터는 메모리 주소를 값으로 갖는 타입이다.
    • int타입 변수 a가 있을 때 a는 메모리에 저장되어 있고 속성으로 메모리 주소를 갖고 있다.
    • 변수 a의 주소가 0x0100번지라면 메모리 주솟값도 숫자값이기 때문에 다른 변수의 값으로 사용될 수 있다.
    • 이런 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 포인터 변수 라고 한다.

 

  • 그림에서 int 타입 변수 a의 메모리 주소는 0x0100 번지이고, 값으로 3을 갖는다.
p = &a
  • 이렇게 포인터 변수 p에 a의 주소를 대입할 수 있다.
    • 포인텨 변수 p의 값은 변수 a의 주소인 0x0100이 되고, 이것을
    • “포인터 변수 p가 변수 a를 가리킨다” 고 한다.
  • 메모리 주소를 값으로 가져 메모리 공간을 가리키는 타입을 포인터 라고 한다.
  • 포인터를 이용하면 여러 포인터 변수가 하나의 메모리 공간을 가리킬 수도 있고,
  • 포인터가 가리키고 있는 메모리 공간의 값을 읽을 수도 변경할 수도 있다.

 

14.1.1 포인터 변수 선언

  • 포인터 변수는 가리키는 데이터 타입 앞에 *를 붙여서 선언한다.
var p *int
  • 포인터 변수가 변수 a의 메모리 주소를 값으로 가진다.
var a int
var p *int // int 타입 데이터의 메모리 주소를 가리키는 포인터 변수 
p = &a     // a의 메모리 주소를 포인터 변수 p에 대입 
  • p를 이용해서 변수 a의 값을 변경할 수 있다.
*p = 20
  • 이렇게 하면 p가 가리키는 메모리 공간의 값을 20으로 변경한다.
package main

import "fmt"

func main() {
	var a int = 500
	var p *int // int 포인터 변수 p 선언

	p = &a // a의 메모리 주소를 변수 p의 값으로 대입

	fmt.Printf("p의 값 : %p\\n", p)
	fmt.Printf("p가 가리키는 메모리의 값 : %d\\n", *p)

	*p = 100
	fmt.Printf("a의 값 : %d\\n", a)

}

p의 값 : 0x140000140c8
p가 가리키는 메모리의 값 : 500
a의 값 : 100

 

 

14.1.2 포인터 변숫값 비교하기

  • == 연산을 이용해 포인터가 같은 메모리 공간을 가리키는지 확인할 수 있다.
package main

import "fmt"

func main() {
	var a int = 10
	var b int = 20

	var p1 *int = &a
	var p2 *int = &a
	var p3 *int = &b

	fmt.Printf("p1 == p2 : %v\\n", p1 == p2)
	fmt.Printf("p2 == p3 : %v\\n", p2 == p3)
}

p1 == p2 : true
p2 == p3 : false

14.1.3 포인터의 기본값 nil

  • 포인터 변숫값을 초기화 하지 않으면 기본값은 nil이다. 이 값은 0이지만 정확한 의미는, 유효하지 않는 메모리  주솟값.
  • 어떤 메모리 공간도 가리키고 있지 않다.
var p *int
if p != nil {
	//만약 p가 유효한 메모리 주소를 가리킨다면 true
}

14.2 포인터는 왜 쓰나?

  • 변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에
  • 많은 메모리 공간을 사용한다.
  • 그리고 큰 메모리 공간을 복사할때 성능 문제가 발생한다.
  • 또 다른 공간으로 복사하기 때문에 변경사항이 적용되지 않는다.
  • 아래와 같이 하면 매개변수로 메모리 주소를 전달받아 직접 data 변수를 가리키고 변경하게 된다.
  • 이때 메모리 주소만 복사되기 때문에 전체 변수가 아닌, 메모리 주솟값인 8바이트만 복사된다.
  • 효율적으로 데이터를 조작할 수 있다.
package main

import "fmt"

type Data struct {
	value int
	data  [200]int
}

func ChangeData(arg *Data) { // 매개변수로 Data 포인터를 받는다.
	arg.value = 999
	arg.data[100] = 999 // arg 데이터를 변경
}

func main() {
	var data Data

	ChangeData(&data) // 매개변수에 data의 메모리 주솟값을 넣어준다.
	fmt.Printf("value = %d\\n", data.value)
	fmt.Printf("data[100] = %d\\n", data.data[100]) // data의 두 필드 출력
}

value = 999
data[100] = 999

14.2.1 Data 구조체를 생성해 포인터 변수 초기화하기

  • 구조체 변수를 별도로 생성하지 않고 곧바로 포인터 변수에 구조체를 생성해 주소를 초깃값으로 대입할 수도 있다.

14.3. 인스턴스

  • 인스턴스는 메모리에 할당된 데이터의 실체이다.
  • 아래 코드는 Data 타입값을 저장할 수 있는 메모리 공간을 할당한다.
var data Data
  • 이렇게 할당된 메모리 공간의 실체를 인스턴스 라고 부른다.
var data Data
var p *Data = &data
  • Data 타입 포인터 변수 p를 선언하고 data의 주소를 대입한다.
  • 이때 p는 data를 가리키는데, p가 생성될 때 새로운 Data 인스턴스가 만들어진게 아니라, 기존에 있던 data 인스턴스를 가리킨 것이다.
  • 그러니 여기서 Data 인스턴스는 한 개이다.

  • 인스턴스를 별도로 생성하지 않고 곧바로 인스턴스를 생성해 주소를 포인터 변수에 초깃값으로 대입할 수 있다.
var p *Data = &Data{}

14.3.1 인스턴스는 데이터의 실체다.

  • 구조체 포인터를 함수 매개변수로 받는 다는 말은 구조체 인스턴스로 입력을 받겠다는 뜻이다.

14.3.2 new() 내장 함수

  • new 내장 함수를 이용하면 더 간단히 별도의 변수를 선언하지 않고 초기화할 수 있다.
p1 := &Data{}           // &를 사용하는 초기화
var p2 = new(Data)      // new()를 사용하는 초기화 
  • new 내장함수는 인수로 타입을 받는다. 타입을 메모리에 할당하고 기본값으로 채워 그 주소를 반환한다.

14.3.3 인스턴스는 언제 사라지나

  • Go 언어는 가비지 컬렉터를 제공한다.
  • 포인터 변수가 없어져서 인스턴스를 가리키지 않게 되면 쓸모가 없다.
  • 가비지 컬렉터는 다음 번 청소를 할 때 이 쓸모없어진 인스턴스를 지운다.

14.4. 스택 메모리와 힙 메모리

  • 이론상 스택 메모리 영역이 힙 메모리 영역보다 효율적이다.
  • 하지만 스택은 함수 내부 에서만 사용 가능한 영역이다.
  • 그래서 함수 외부로 공개되는 메모리 공간은 힙 메모리 공간을 할당한다.
  • 함수 외부로 공개되는 인스턴스의 경우 함수가 종료되어도 사라지지 않는다.
  • Go는 메모리 공간이 함수 외부로 공개되는지를 자동으로 검사해서 스택, 힙 메모리에 할당할지 결정한다.

 

15장. 문자열

15.1. 문자열

  • 문자열은 문자집합이다.
  • string
  • 큰따옴표 나 백쿼트 back quote로 묶어서 표시한다.
  • 큰따옴표와 백쿼트는 쓰임이 다르다.
  • 백쿼트로 문자열을 묶으면 문자열 안의 특수 문자가 일반 문자처럼 처리된다.
package main

import "fmt"

func main() {
	str := "Hello\\t'World'\\n" // 큰 따옴표로 묶으면 특수 문자가 동작한다. 
	str2 := `Go is "awesome"!\\nGo is simple and\\t 'powerful'` // 특수문자가 동작하지 않는다.
	fmt.Println(str1)
	fmt.Println(str2)
}
  • 백쿼트로 묶으면 여러 줄에 걸쳐서 문자열을 쓸 수 있다.
  • 큰따옴표로 묶으면 \n 을 사용해야 한다.

15.1.1 UTF-8 문자 코드

  • Go는 UTF-8 문자코드를 표준 문자 코드로 사용한다.
  • UTF-16과달리 한 문자에 2바이트 고정사용이 아니라 자주 사용되는 영문자, 숫자, 일부 특수문자를 1바이트로 표현한다.

15.1.2 rune 타입으로 한 문자 담기

  • “문자 하나”를 표현하는데 rune 타입을 사용한다.
  • Go 언어 기본 타입에서 3바이트 정수 타입은 제공되지 않기 때문에 rune 타입은 4바이트 정수 타입인 int32 타입의 별칭 타입이다.
package main

import "fmt"

func main() {
	var char rune = '한'

	fmt.Println("%T\\n", char) // chart 타입 출력
	fmt.Println(char)         // char 값 출력
	fmt.Printf("c\\n", char)   // 문자 출력
}

// int32
// 54620
// 한

15.1.3 len()으로 문자열 크기 알아내기

  • len() 내장 함수를 이용해서 문자열 크기를 알 수 있다.
  • 크기는 “문자 수” 가 아니라 “문자열이 차지하는 메모리 크기” 다
package main

import "fmt"

func main() {
	str1 := "가나다라마"
	str2 := "abcde"

	fmt.Printf("len(str1) = %d\\n", len(str1))
	fmt.Printf("len(str2) = %d\\n", len(str2))
}

len(str1) = 15
len(str2) = 5

15.1.4 []rune 타입 변환으로 글자 수 알아내기

package main

import "fmt"

func main() {
	str := "Hello 월드"    // str을
	runes := []rune(str) // []rune 타입 변환이 가능하다.

	fmt.Printf("len(str) = %d\\n", len(str))
	fmt.Printf("len(runes) = %d\\n", len(runes))
}
len(str) = 12
len(runes) = 8

 

 

15.2 문자열 순회

  1. 인덱스를 사용한 바이트 순회
  2. []rune 타입 변환 후 한 글자씩 순회
  3. range 키워드를 이용한 한 글자씩 순회

15.2.1 인덱스를 사용해 바이트 단위 순회하기

  • 인덱스를 사용해 직접 접근하기
  • 인덱스를 사용해 각 바이트 값을 출력하는대, 한글은 깨진다.
  • str[i]처럼 인덱스로 접근하면 요소의 타입은 uint8 즉, 바이트이다.
  • 그래서 1바이트 크기인 영문은 잘 표시되는데, 3바이트 크기인 한글은 깨져서 표시된다.
package main

import "fmt"

func main() {
	str := "Hello 월드 !"                // 한영이 섞인 문자열
	println("size of str :", len(str)) // 문자열 글자 갯수가 아닌 바이트 크기

	for i := 0; i < len(str); i++ { // 문자열 크기를 얻어 순회
		fmt.Printf(" 타입:%T 값:%d 문자값:%c\\n", str[i], str[i], str[i]) // 바이트 단위로 출력
	}
}

// 타입:uint8 값:72 문자값:H
// 타입:uint8 값:101 문자값:e
// 타입:uint8 값:108 문자값:l
// 타입:uint8 값:108 문자값:l
// 타입:uint8 값:111 문자값:o
// 타입:uint8 값:32 문자값:
// 타입:uint8 값:236 문자값:ì
// 타입:uint8 값:155 문자값:
// 타입:uint8 값:148 문자값:
// 타입:uint8 값:235 문자값:ë // 한글은 깨졌다.
// 타입:uint8 값:147 문자값:
// 타입:uint8 값:156 문자값:
// 타입:uint8 값:32 문자값:
// 타입:uint8 값:33 문자값:!

15.2.2 []rune 타입 변환 후 한 글자씩 순회하기

  • 아래 처럼 한영 문자가 섞인 문자열은 str 문자열을 []rune으로 타입 변환한 다음에 순회한다.
  • 여기서 변수 arr은 한 문자씩 이뤄진 배열이기 때문에 len()은 문자열 글자 갯수를 반환한다.
  • 이렇게 하면 for문을 이용해 각 글자를 돌면서 순회할 수 있다.
  • 하지만 이 경우, []rune으로 변환되는 과정에서 별도의 배열을 할당하므로 불필요한 메모리를 사용하게 된다.
package main

import "fmt"

func main() {
	str := "Hello 월드!"
	arr := []rune(str)

	fmt.Println(arr)

	for i := 0; i < len(arr); i++ {
		fmt.Printf(" 타입:%T 값:%d 문자값:%c\\n", arr[i], arr[i], arr[i])
	}
}

[72 101 108 108 111 32 50900 46300 33]
 타입:int32 값:72 문자값:H
 타입:int32 값:101 문자값:e
 타입:int32 값:108 문자값:l
 타입:int32 값:108 문자값:l
 타입:int32 값:111 문자값:o
 타입:int32 값:32 문자값: 
 타입:int32 값:50900 문자값:월
 타입:int32 값:46300 문자값:드
 타입:int32 값:33 문자값:!

15.2.3 range 키워드를 이용해 한 글자씩 순회하기

  • range를 이용해 순회한다.
  • 인덱스값은 사용하지 않기 때문에 밑줄 _ 을 통해 무효화한다.
  • 모든 문자 타입이 int32, 즉 rune 이다.
  • rune은 기본적으로 숫자값이기 때문에 어떤 수인지 출력하고, %c을 통해 해당 문자를 출력한다.
  • 이렇게 range를 사용하면 추가 메모리 할당 없이 문자열을 한 글자씩 순회할 수 있다.
package main

import "fmt"

func main() {
	str := "Hello 월드!"      // 한영 문자가 섞인 문자열
	for _, v := range str { // range를 이용한 순회
		fmt.Printf(" 타입:%T 값:%d 문자:%c\\n", v, v, v) // 출력
	}
}
타입:int32 값:72 문자:H
타입:int32 값:101 문자:e
타입:int32 값:108 문자:l
타입:int32 값:108 문자:l
타입:int32 값:111 문자:o
타입:int32 값:32 문자: 
타입:int32 값:50900 문자:월
타입:int32 값:46300 문자:드
타입:int32 값:33 문자:!

 

 

15.3 문자열 합치기

  • +와 -을 이용해서 문자열을 이을 수 있다.
str := str1 + " " + str2
str1 += " " + str2

15.3.1 문자열 비교하기

  • ==, != 을 사용해서 문자열이 같은지 비교할 수 있다.

15.3.2 문자열 대소 비교하기

  • , < , >=, <= 연산자를 이용해서 문자열 간 대소 를 비교할 수 있다.
  • 첫 글자부터 하나씩 값을 비교해서 그 글자에 해당하는 유니코드 값이 다를 경우 대소를 반환한다.
  • 문자열 길이와 상관없이 앞글자부터 같은 위치에 있는 글자끼리 비교한다.

15.4 문자열 구조

15.4.1 string 구조 알아보기

  • reflect 패키지 안의 StringHeader 구조체를 통해 내부 구현을 엿볼 수 있다.
type StringHeader struct {
	Data uintptr
	Len  int
}
  • string은 필드가 2개인 구조체이다.
  • 첫 번째 필드 Data는 uintptr 타입으로 문자열의 데이터가 있는 메모리 주소를 나타내는 포인터이다.
  • 두 번째 필드 Len은 int 타입으로 문자열의 길이를 나타낸다.

15.4.2 string 끼리 대입하기

package main

import "fmt"

func main() {
	str1 := "안녕하세요. 한글 문자열입니다."
	str2 := str1

	fmt.Printf(str1)
	fmt.Printf("\\n")
	fmt.Printf(str2)
}
안녕하세요. 한글 문자열입니다.
안녕하세요. 한글 문자열입니다.
  • 구조체 변수가 복사될 때 구조체 크기만큼 메모리가 복사된다.
  • str1, str2 모두 구조체이므로 각 필드 , 즉 Data 포인터값과 Len 값이 복사된다.
  • 그러니 str1의 Data와 Len값만 str2 에 복사한다.
  • Data는 문자열 주솟값이므로 문자열 자체는 복사되지 않는다.

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	str1 := "Hello World!"
	str2 := str1 // str1 변숫값을 str2에 복사

	stringHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) // Data값 추출
	stringHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) // Data값 추출

	fmt.Println(stringHeader1) // 각 필드값을 출력
	fmt.Println(stringHeader2)

}

&{4363915770 12}
&{4363915770 12}
  • str1 변숫값을 str2에 복사하여 같은 메모리 데이터를 가리키게 된다.
  • string 타입 str1, 2를 *reflect.StringHeader 값으로 변환한다.
  • Go는 string에서 *reflect.StringHeader 타입으로 변환을 막고 있기 때문에 ,
  • 강제 변환을 위해서 unsafe.Pointer(&str1)를 사용해서 먼저 unsafe.Pointer 타입으로 변환한 다음에
  • 다시 *reflect.StringHeader 타입으로 변환한다.
  • 출력 시 string 변수의 Data 값이 같다. string 변수가 가리키는 문자열이 아무리 길어도, string 변수끼리 대입 연산에서는 16바이트 값만 복사될 뿐 문자열 데이터는 복사되지 않는다
  • 성능 문제가 없다.

15.5 문자열은 불변이다.

  • 문자열은 불변 immutable 이다.
  • string 타입이 가리키는 문자열의 일부만 변경할수는 없다.
var str string = "Hello World"
str = "How are you?" // 전체 바꾸기는 가능하다. 
str[2] = 'a' // ERROR가 뜬다. 일부 바꾸기는 불가능하다. 
  • 만약 슬라이스로 타입변환시 , 문자열을 복사해서 새로운 메모리 공간을 만들어 슬라이스가 가리키도록 한다.

15.5.1 문자열 합산

  • 또한 문자열을 합산할 때도 기존 문자열 메모리 공간을 건드리지 않고
  • 새로운 메모리 공간을 만들어서 두 문자열을 합친다.
  • 그러므로 string 합 연산 이후 주솟값이 변경된다.
  • 그러므로 합 연산을 자주 할 시 메모리가 낭비된다.
package main

import (
	"fmt"
	"strings"
)

func ToUpper1(str string) string {
	var rst string
	for _, c := range str {
		if c >= 'a' && c <= 'z' {
			rst += string('A' + (c - 'a')) // 합 연산 사용

		} else {
			rst += string(c)
		}
	}
	return rst
}

func ToUpper2(str string) string {
	var builder strings.Builder
	for _, c := range str {
		if c >= 'a' && c <= 'z' {
			builder.WriteRune('A' + (c - 'a')) // strings.Builder 사용

		} else {
			builder.WriteRune(c)
		}
	}
	return builder.String()
}

func main() {
	var str string = "Hello World"

	fmt.Println(ToUpper1(str))
	fmt.Println(ToUpper2(str))
}

HELLO WORLD
HELLO WORLD
  • strings 패키지의 Builder 를 이용해서 메모리의 낭비를 줄일 수 있다.
  • ToUpper1 함수는 합 연산을 사용해서 문자를 더한다.
  • 하지만 이때 합연산 마다 메모리 공간을 새로 할당해서 더하므로 메모리 공간이 버려진다.
  • ToUpper2 함수는 strings.Builder 객체를 이용해서 문자를 더한다.
  • builder 는 내부에 슬라이스를 갖고 있기 때문에, WriteRune() 메서드를 통해 문자를 더할 때 매번 메모리를 새로 생성하지 않고 기존 메모리 공간에 빈 자리가 있으면 그냥 더한다.
  • 그렇게 메모리 공간 낭비를 없앨 수 있다.

 

 

16장. 패키지

16.1 패키지

  • 패키지는 Go 언어에서 코드를 묶는 가장 큰 단위이다.
  • 함수로 코드 블록을
  • 구조체로 데이터를
  • 패키지로 함수와 구조체와 그 외 코드를 묶는다.
  • main 패키지는 특별한 패키지로 프로그램 시작점을 포함한다.
  • 프로그램은 main 패키지 하나와 외부 패키지로 구성된다.

16.1.1 main 패키지

  • 프로그램 시작점을 포함한 패키지
  • 프로그램이 실행되면 프로그램을 메모리로 로드하는데
  • 시작점부터 한 줄씩 코드를 실행하게 된다.
  • 시작점이 main() 함수이다.

16.1.3 유용한 패키지 찾기

16.2 패키지 사용하기

  • 외부 노출 여부는 변수명, 함수명, 구조체 명의 첫 글자가 대문자면 노출, 소문자면 노출되지 않는다.
  • 패키지명은 가져오는 패키지 경로의 가장 마지막 폴더명이다.

16.2.1 임포트하기

import fmt

import (
	"fmt"
	"os"
)

16.2.2 패키지 멤버에 접근하기

fmt.Println("Hello World")

16.2.3 경로가 있는 패키지 사용하기

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Println(rand.Int())
}

16.2.4 겹치는 패키지 문제 별칭으로 풀기

  • 패키지명이 겹치면 별칭을 줘서 구별해주기
import (
	"text/template"
	"html/template"
)

import (
	"text/template"
	htemplate "html/template" // 별칭 htemplate
)

16.2.5 사용하지 않는 패키지 포함하기

  • 패키지를 임포트하고 나서 사용하지 않으면 에러가 발생한다.
  • 패키지를 직접 사용하지 않지만 부가효과를 얻고자 임포트하는 경우에는 _ 를 사용한다.
import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3" // _ 을 아용해 오류 방지 
) 

16.2.6 패키지 설치하기

  • go build 를 통해서 빌드할 때는 해당하는 패키지를 찾아서 포함한다음 실행 파일을 생성한다.
  • Go 가 import된 패키지를 찾는 방법
    1. Go 설치 경로. Go를 설치할 때 기본 패키지까지 같이 설치된다. 그래서 기본 패키지는 Go 설치경로에 포함되어 있다.
    2. 깃 허브와 같은 외부 저장소에 저장된 패키지의 경우 외부 저장소에서 다운받아 GOPATH\pkg 경로에 설치하고, 모듈에 정의된 패키지 버전에 맞게 다운로드 한다.
    3. 현재 모듈아래 위치한 패키지인지 검사한다.

16.3 Go 모듈

  • Go 모듈은 Go 패키지를 모아놓은 프로젝트 단위이다.
  • 모든 Go 코드는 Go 모듈 아래 있어야 한다.
  • go build를 하려면 반드시 Go 모듈 루트 폴더에 go.mod 파일이 있어야 한다.
  • go.mod 파일에는 모듈 이름, Go 버젼, 외부 패키지가 명시되어 있다.
  • go build - go.mod, go.sum 으로 패키지들을 합쳐서 실행파일 생성
go mod init [패키지명] // 으로 Go 모듈 생성

custompkg.go

package custompkg

import "fmt"

func PrintCustom() {
	fmt.Println("This is custom package!")
}

usepkg.go

pakcage main

import (
	"fmt"
	"goproject/usepkg/custompkg"
	
	"github.com/guptarohit/asciigraph"
	"github.com/tuckersGo/musthaveGo/ch16/expkg"
)

func maint() {
	custompkg.PrintCustom()
	expkg.PrintSample()
	
	data := []float64(3, 4, 5, 6, 9, 7, 5, 8, 5, 10, 2, 7, 2, 5, 6}
	graph := asciigraph.Plot(data)
	fmt.Println(graph)
}
  • go mod tidy를 실행하면 Go 모듈에 필요한 패키지를 찾아서 다운로드 하고 필요 정보를 go.mod, go.sum 파일에 적는다.
  • go.sum에는 패키지 위조 여부를 검사하기 위한 체크섬결과가 담겨있다.
go mod tidy
go build
usepkg
  • 다운 받은 패키지들은 GOPATH/pkg/mod 폴더에 저장되며 재사용된다.

16.4 패키지명과 패키지 외부 공개

  • Go에서 패키지명의 모든 문자는 소문자로 할 것을 권장한다.
  • 패키지 전역으로 선언된 첫 글자가 대문자로 시작되는 모든 변수, 상수, 타입, 함수, 메서드는 패키지 외부로 공개된다.
  • 대문자로 시작하더라도 포함된 구조체가 소문자로 시작하면 패키지 외부로 공개되지 않는다.
  • 이런 경우 패키지 외부에서 호출할 수 없다.

16.5 패키지 초기화

  • 패키지를 임포트하면 컴파일러는 패키지 내 전역 변수를 초기화한다.
  • 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 한다.
  • init() 함수는 반드시 매개변수와 반환값이 없어야 한다.
  • 어떤 패키지의 초기화 함수 init() 함수 기능만 사용하기 원할 경우 _ 를 이용해 임포트 한다.

 

17장. 숫자 맞추기 게임 만들기

17.1 해법

  1. 먼저 0~99 사이의 랜덤한 숫자 하나를 정한다.
  2. 사용자 입력을 받는다.
  3. 입력값과 랜덤값을 받는다. 만약 사용자 입력 숫자가 더 크다면 ”입력히신 숫자가 더 큽니다”를 출력하고 작다면 “ 입력하신 숫자가 더 작습니다” 를 출력한다. 다시 사용자 입력을 받아서 반복한다.
  4. 만약 숫자가 맞으면 ”축하합니다. 숫자를 맞추셨습니다. 시도횟수 : 13번” 같이 메시지를 출력한다.
  5. 프로그램을 종료한다.

17.2 사전 지식

17.2.1 math/rand 패키지

  • 랜덤한 숫자를 얻으려면 math/rand 패키지
  • 특정 범위에서 int 타입 랜덤값을 생성하는 rand.Intn(range) 함수 사용
  • 랜덤값이 서로 다른 랜덤값으로 산출되려면 랜덤 시드를 다른 값으로 생성해줘야 한다.

17.2.2 time 패키지

  • 현재 시각을 랜덤 시드 값으로 설정해주면 매번 다른 랜덤값을 생성할 수 있다.
  • 랜덤 시드값은 int64 타입이므로 Time 객체의 메서드인 UnixNano() 메서드를 통해서 int64로 변환한다.
func (t Time) UnixNano() int64

17.3 랜덤한 숫자 생성하기

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano()) // Seed 함수의 입력값 타입은 int64이며, UnixNano() 메서드를 이용해서 int64로 변경한다. 

	n := rand.Intn(100) // 이때 100은 범위이다. 0에서 범위에서 1을 뺀 값 사이에서 0~99
	fmt.Println(n)
}

17.4 숫자값 입력받기

package main

import (
	"bufio"
	"fmt"
	"os"
)

var stdin = bufio.NewReader(os.Stdin)

func InputIntValue() (int, error) {
	var n int
	_, err := fmt.Scanln(&n) // int 타임 값을 입력받음
	if err != nil {
		stdin.ReadString('\\n') // 에러 발생시 입력 스트림을 비움
	}
	return n, err
}

func main() {
	for {
		fmt.Println("숫자값을 입력하세요")
		n, err := InputIntValue()
		if err != nil {
			fmt.Println("숫자만 입력하세요")
		} else {
			fmt.Println("입력하신 숫자는 ", n, " 입니다.")
		}
	}
}

17.5 숫자 맞추기 완성하기

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"time"
)

var stdin = bufio.NewReader(os.Stdin)

func InputIntValue() (int, error) {
	var n int
	_, err := fmt.Scanln(&n)
	if err != nil {
		stdin.ReadString('\\n')
	}
	return n, err
}

func main() {
	rand.Seed(time.Now().UnixNano())

	r := rand.Intn(100) // 랜덤값 생성
	cnt := 1
	for {
		fmt.Println("숫자값을 입력하세요")
		n, err := InputIntValue()
		if err != nil {
			fmt.Println("숫자만 입력하세요")
		} else {
			if n > r {
				fmt.Println("입력하신 숫자가 더 큽니다.")
			} else if n < r {
				fmt.Println("입력하신 숫자가 더 작습니다.")
			} else {
				fmt.Println("숫자를 맞췄습니다. 축하합니다. 시도한 횟수 :", cnt)
				break
			}
			cnt++
		}
	}
}

 

반응형