a story

Go스터디: 3주차(12~17장) 본문

Book Study/Tucker의 Go Programming

Go스터디: 3주차(12~17장)

한명 2023. 10. 15. 16:54

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

12. 배열

배열(array)은 같은 타입의 데이터들로 이루어진 타입이다. 배열의 각 값은 요소(element)라고 하고, 이를 가리키는 위치값을 인덱스(index)라고 한다.

// var 변수명 [요소개수]타입
var t [5]float64
days := [3]string{"monday","tuesday","wednesday"}
x := [...]int{10,20,30} // 요소 개수 생략
var b = [2][5]int{ // 다중 배열
	{1,2,3,4,5},
	{6,7,8,9,10}, // 초기화 시 닫는 중괄호 } 가 마지막 요소와 같은 줄에 있지 않은 경우 마지막 항목 뒤에 쉼표, 를 찍어줘야 함!
} // 추후 항목이 늘어날 경우 쉼표를 찍지 않아서 생길 수 있는 오류를 방지하기 위해 존재하는 규칙

배열 선언 시 개수는 항상 상수여야 한다. 아니면 에러 발생!

package main

func main() {
	x := 5
	b := [x]int{1, 2, 3, 4, 5} // invalid array length x
}

 

배열 순회

package main

import "fmt"

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	for _, v := range a {
		fmt.Println(v)
	}

	var b = [2][5]int{ // 다중 배열
		{1, 2, 3, 4, 5},
		{6, 7, 8, 9, 10},
	}

	for _, b1 := range b {
		for _, b2 := range b1 {
			fmt.Print(b2, " ")
		}
		fmt.Println()
	}
}

배열의 핵심

  1. 배열은 연속된 메모리다.
  2. 컴퓨터는 인덱스와 타입 크기를 사용해서 메모리 주소를 찾는다.

 

중요 공식

요소 위치 = 배열 시작 주소 + (인덱스 X 타입크기)
→ a 배열 시작 주소가 100번지 라면 a[3] 주소는 100 + (3*4) = 112번지

배열 크기 = 타입크기 X 항목개수
→ [5]int = 8*5 = 40bytes

왜 두 공식의 int 타입크기를 다르게 한거지? 오타?

 

13. 구조체

여러 필드(field)를 묶어서 하나의 구조체(structure)를 만든다. 구조체는 다른 타입의 값들을 변수 하나로 묶어준다.

프로그래밍의 역사는 객체 간 결합도(객체 간 의존관계)는 낮추고, 연관있는 데이터 간 응집도를 올리는 방향으로 흘러왔다. 함수와 구조체 모드 응집도를 증가시키는 역할을 한다.

→ 구조체의 등작으로, 프로그래머는 개별 데이터의 조작/연산보다는 구조체 간의 관계와 상호작용 중심으로 변화하게 되었다.

type 타입명 struct {
	필드명 타입
}

학생(Student) 구조체를 만들고, 이름, 반과 같은 정보를 넣는다. 구조체의 각 필드는 .을 통해서 접근할 수 있다.

package main

import "fmt"

func main() {
	type Student struct {
		name  string
		class int
	}
	var std1 Student

	std1.name = "홍"
	std1.class = 1

	fmt.Println(std1.name, std1.class)

	// 초기화를 아래와 같이 해줄 수도 있다.
	std2 := Student{"김", 2}

	fmt.Println(std2.name, std2.class)
}

구조체를 포함하는 구조체를 만들 때,

내장 타입처럼 포함할 때는 instance.StructName.fieldName 으로 접근한다.

포함된 필드 방식으로 사용할 수 있는데, 이때는 instance.fieldName으로 포함된 구조체 필드를 바로 접근할 수도 있다.

 

 

필드 배치 순서에 따른 구조체 크기 변화

구조체의 인스턴스가 생성되면 구조체 필드의 크기를 더한 만큼의 메모리 공간을 차지하게 된다.

다만 필드 배치 순서에 따라 구조체 크기가 달라 질 수 있는데, 메모리 정렬(Memory Alignment)를 고려해서 8의 배수인 메모리 주소에 데이터를 할당하도록 동작하기 때문에, 만약 적은 사이즈의 필드가 있는 경우는 메모리 패딩(memory Padding)을 넣고, 다음 8배수 공간에 데이터를 할당하기 때문이다.

메모리 정렬이란 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는 것을 말한다.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	type User struct {
		Age   int32
		Score float64
	}

	user := User{23, 77.2}

	fmt.Println(unsafe.Sizeof(user.Age), unsafe.Sizeof(user.Score)) // 4, 8
	fmt.Println(unsafe.Sizeof(user)) // 16
}

이러한 이유로 구조체에서 메모리 패딩을 고려한 필드 배치방법을 사용해야 한다.

→ 8 bytes 보다 작은 필든느 8 bytes 크기(단위)를 고려해서 몰아서 배치하자.

 

14. 포인터

포인터는 메모리 주소를 값으로 갖는 타입이다. 포인터 변수를 초기화 하지 않으면 기본 값은 nil이다.

package main

import (
	"fmt"
)

func main() {
	var a int
	var p1 *int
	p1 = &a // a의 메모리 주소를 포인터 변수 p에 대입

	var p2 *int = &a

	fmt.Println(p1 == p2) // == 연산을 사용해 포인터가 같은 메모리 공가늘 가리키는 지 확인
}

포인터는 왜 쓸까?

→ 변수 대입이나 함수 인수 전달은 항상 값 복사를 하기 때문에, 메모리 공간을 사용하는 문제와 큰 메모리 공간을 복사할 때 발생하는 성능 문제가 있다.

→ 또한 다른 공간으로 복사되기 때문에 실제 변수의 값에 변경 사항이 적용되지 않는다.

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

구조체 변수를 별도로 생성하지 않고, 곧바로 포인터 변수에 구조체를 생성해 주소를 초기값으로 대입하는 방법

방식1) Data타입 구조체 변수 data를 선언하고, data변수의 주소를 반환하여 대입

var data Data
var p *Data = &data

방식2) *Data 타입 구조체 변수 p를 선언하고, Data 구조체를 만들어서 주소를 반환

var p *Data = &Data{}

→ 이렇게 하면 포인터 변수 p만 가지고도 구조체의 필드값에 접근하고 변경할 수 있다. (실제 Data 구조체에 대한 변수를 생성하지 않음)

방식3) new() 내장 함수: 포인터값을 별도의 변수를 선언하지 않고 초기화 할 수도 있는데, new 내장 함수를 이용하면 더 간단히 표현할 수 있다.

p1 := &Data{} // &를 사용하는 초기화
var p2 = new(Data) // new()를 사용하는 초기화

 

인스턴스

인스턴스란 메모리에 할당된 데이터의 실체이다. 포인터를 이용해서 인스턴스에 접근할 수 있다.

구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 것과 동일하다.

Go 언어는 가비지 컬렉터(Garbage Collector)라는 메모리 정리 기능을 제공하는데, 카비지 컬렉터가 일정 간격으로 메모리에 쓸모 없어진 데이터를 정리한다.

쓸모 없어진 데이터: 아무도 찾지 않는 데이터는 쓸모 없는 데이터다. 예를 들어, 함수가 종료되면 함수에서 사용한 인스턴스는 더 이상 쓸모가 없게 된다.

 

 

스택 메모리와 힙 메모리

대부분의 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용한다. 이론상 스택 메모리 영역이 효율적이지만, 스택 메모리는 함수 내부에서만 사용 가능한 영역이다. 그래서 함수 외부로 공개되는 메모리 공간은 힙 메모리 공간을 할당한다.

자바는 클래스 타입을 힙에, 기본 타입을 스택에 할당한다. Go에서는 탈출 검사(escape anlaysis)를 해서 어느 메모리에 할당할지 결정한다. 즉 Go 언어는 어떤 타입이나 메모리 할당 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 메모리에 할당할지 힙 메모리에 할당할지 결정한다.

package main

import (
	"fmt"
)

type User struct {
	Name string
	Age  int
}

func NewUser(name string, age int) *User {
	var u = User{name, age}
	return &u // 보통 함수가 종료하면, 함수 내 선언된 변수는 사라진다. 탈출 분석을 통해 u 메로리가 사라지지 않음
}

func main() {
	userPointer := NewUser("AAA", 22)
	fmt.Println(userPointer)
}

Go의 스택 메모리는 계속 증가되는 동적 메모리 풀로, 일정한 크기를 같는 C/C++과 비교해 메모리 효율성 높고, 스택 고갈 문제도 발생하지 않는다.

 

15. 문자열

문자열은 문자 집합을 나타내는 타입으로 string이다. 문자열은 큰 따옴표나 백쿼트로 묶어서 표시한다.

Go는 UTF-8 문자코드를 표준 문자 코드로 사용한다. UTF-8은 자주 사용되는 영문자, 숫자 일부 특수문자를 1바이트로 표현하고, 그 외 다른 문자들은 2~3바이트로 표현한다. (한글 사용 가능)

 

rune 타입

문자 하나를 표현하기 위해 rune 타입을 사용한다. UTF-8은 한 글자가 1~3바이트 크기이기 때문에, UTF-8 문자값을 가지려면 3바이트가 필요하다. Go에서는 기본 타입에서 3 바이트 정수 타입은 제공되지 않기 때문에 rune 타입은 4 바이트 정수 타입인 int32 타입의 별칭 타입이다.

package main

import (
	"fmt"
)

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

	fmt.Printf("len(str) = %d\\n", len(str))     // string 타입 길이,12 
	fmt.Printf("len(runes) = %d\\n", len(runes)) // []rune 타입 길이, 8
}

string에서 영문은 1바이트, 한글은 3바이트이므로, 총 12가 된다.

string 타입을 []rune으로 변환하면, 각 글자들로 이뤄진 배열로 변환된다. 그래서 각각이 1바이트로 8이 된다.

 

[]byte 타입

string 타입과 []byte 타입은 상호 타입 변환이 가능하다. []byte는 byte 즉 1바이트 부호 없는 정수 타입의 가변 길이 배열이다. 문자열은 메모리에 있는 데이터고, 메모리는 1바이트 단위로 저장되기 때문에 모든 문자열은 1바이트 배열로 변환 가능하다.

파일을 쓰거나, 네트워크로 데이터를 전송하는 경우, io.Writer 인터페이스를 사용하고, io.Writer 인터페이스는 []byte 타입을 인수로 받기 때문에 []byte 타입으로 변환해야 한다. 문자열을 쉽게 전송하고자 string에서 []byte 타입으로 변환을 지원한다.

 

문자열 구조

string은 필드가 1개인 구조체이다. 첫번째 필드 Data는 uintptr 타입으로 문자열의 데이터가 있는 메모리 주소를 나타내는 일정의 포인터이고, 두번째 필드 Len은 문자열의 길이를 나타낸다.

type StringHeader struct {
	Data uintptr
	Len int
}

→ str2 변수에 str1 변수를 대입하면, str1의 Data와 Len값만 str2에 복사한다. 문자열 자체가 복사되지는 않는다.

문자열과 immutable

string 타입이 가리키는 문자열의 일부만 변경 할 수 없다. 변경하려면 슬라이스로 타입 변환하고, 변경하는 방식을 취할 수 있다.

package main

func main() {
	str := "Hello world"
	str = "How are you"
	str[2] = 'a' // cannot assign to str[2] (neither addressable nor a map index expression)
}

→ 문자열의 합산을 하면 기존 문자열 메모리 공간을 건드리지 않고, 새로운 메모리 공간을 만들어서 두 문자열을 합치기 때문에 주소값이 변경된다. (문자열 불변 원칙이 준수 된다.)

이로 인해 메모리 낭비가 있을 수 있는데, strings 패키지의 Builder를 이용해서 메모리 낭비를 줄일 수 있다.

 

16. 패키지

16.01. 패키지

패지키(package)란 Go에서 코드를 묶는 가장 큰 단위 이다.

함수로 코드 블록을, 구조체로 데이터를, 패키지로 함수와 구조체와 그 외 코드를 묶는다. main 패키지는 특별한 패키지로 프로그램 시작점을 포함한 패키지이다. main() 함수와 다른 함수, 구조체 등을 가진다. 외부 패키지는 main() 을 가지지 않는다.

한 프로그램은 main 패키지 외에 다수의 다른 패키지를 포함할 수 있다.

이러한 패키지를 import해 사용한다.

패키지를 가져오면 해당 패키지명을 쓰고 . 연산자를 사용해 패키지에서 제공하는 함수, 구조체 등에 접근할 수 있다.

import (
	"fmt"
	"math/rand"
	"text/template"
	htemplate "html/template" // 동일한 패키지 명에는 별칭 htemplate
	_ "github.com/mattn/go-sqllite3" // 패키지를 import하면 무조건 사용해야한다. 
  // 패키지를 직접 사용하지 않지만 부가효과를 얻는 경우 _ 을 패키지명 앞에 붙여준다.
  // 부과효과: 패키지가 초기화 되면서 실행되는 코드에 따른 효과
)

fmt.Println("Hello World") // fmt 패키지명 . Println() 함수명
fmt.Println(rand.Int()) // 경로가 있느 패키지인 math/rand 패키지의 경우는 마지막 폴더명인 rand만 사용한다.

패키지를 import 하면 컴파일러는 패키지 내 전역 변수를 초기화 한다. 그런 다음 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 한다. init() 함수는 반드시 입력 매개변수가 없고, 반환값도 없는 함수여야 한다.

이때 패키지의 초기화 함수인 init() 함수 기능만 사용하기 원할 경우 밑줄 _을 이용해서 import 한다.

16.02. 모듈

모듈은 패키지를 모아 놓은 Go의 프로젝트 단위로 Go 1.16부터 기본이 됐다.

이전에는 Go 모듈을 만들지 않는Go 코드는 모두 GOPATH/src 아래 폴더 아래에 있어야 했지만, 모듈이 기본이 되면서 모든 Go 코드는 Go 모듈 아래에 있어야 한다.

go build를 사용하려면 반드시 Go 모듈 루트 폴더에 go.mod 파일이 있어야 한다. go build를 통해 실행 파일을 만들 때, go.mod와 외부 저장소 패키지 버전 정보를 담고 있는 go.sum 파일을 통해 외부 패키지와 모듈 내 패키지를 합쳐서 실행 파일을 만든다.

go mod init 패키지명

go.mod : Go 버전과 외부 패키지 등이 명시된 파일
go.sum: 외부 저장소 패키지 버전 정보를 담고 있는 파일, 패키지 위조 여부 검사를 위한 checksum 결과가 있다.

 

모듈 예제

1. goproject/usepkg 폴더를 만든다.

2. go 모듈 생성

go mod init goproject\usepkg

3. goproject/usepkg/custompkg/custompkg

package custompkg

import "fmt"

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

4. goproject/usepkg/usepkg.go

package main

import (
	"fmt" // go가 설치되면 같이 설치되는 표준 패키지
	"goprojects/usepkg/custompkg" // 현재 모듈에 속한 패키지

	"github.com/guptarohit/asciigraph" // 외부 저장소 패키지
	"github.com/tuckersGo/musthaveGo/ch16/expkg"
)

func main() {
	custompkg.PrintCustom()
	expkg.PrintSample()

	data := []float64{3, 4, 5, 5, 5, 2, 13, 5, 8, 6, 4}
	graph := asciigraph.Plot(data)
	fmt.Println(graph)
}

5. go mod tidy 로 모듈에 필요한 패키지를 찾아서 다운로드 해주고, 필요한 패키지 정보를 go.mod 파일과 go.sum 파일에 적어주게 된다. 다운 받은 외부 패키지인 asciigraph 패키지와 expkg 패키지는 GOPATH/pkg/mod 폴더에 버전별로 저장되어 있다.

6. 파일 내용: go.mod와 go.sum에 필요한 패키지의 버전 정보가 기입되어서 항상 같은 버전의 패키지가 사용되므로, 버전 업데이트에 따른 문제가 발생하지 않는다.

// go.mod
module goprojects/usepkg

go 1.20

require (
	github.com/guptarohit/asciigraph v0.5.6
	github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda
)

// go.sum
github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL09Yx6E=
github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda h1:F21GWOayUeFkA47sc6oB2zb6ly9emQUx2A1wHTETta0=
github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda/go.mod h1:o12FpIqEJes/Y7CWE9BJemI9VUTQBsH7t3wYlDCw3Fw=

 

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

숫자 맞추기 게임은 책에서 소개하는 첫번째 프로젝트로 간단히 랜던값을 생성하고, 표준 입력으로 숫자를 받고, 두 값을 비교해서 결과를 출력하는 프로젝트이다.

사용자 입력과 비교를 반복적으로 처리하기 위해 main()함수는 for문으로 구성되어 있다.

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 2주차(3~11장)  (1) 2023.10.08