Tucker의 Go 언어 프로그래밍을 읽고..

suji·2023년 8월 2일
0

Go

목록 보기
2/9
post-thumbnail

실무에서 Go를 익히면서 Go언어 자체에 대한 궁금증과, 기본적인 지식에 대한 갈망을 해결하고자
유명한! Tucker의 Go 언어 프로그래밍 을 읽고 조금씩 정리했던 내용이다.

궁금하신 분들을 위해 빠르게 책 후기부터 공유해보자면,

< 책 후기 >

  • 깔끔하다.
  • 코드 보기가 편하다.
  • 기초 문법을 공부하기 좋다.
  • 실습도 있어서 입문자가 보기 좋을 것 같다.

if 문

  • 초기문; 조건문
// 조건문 검사 전에 초기문을 넣을 수 있음. 초기문은 검사에 사용할 변수를 초기화할 때 주로 사용.
	if filename, success := UploadFile(); success {
		// 초기문에서 선언한 변수의 범위는 if문 안으로 한정됨.
		fmt.Println("Upload Success", filename)
	} else {
		fmt.Println("Failed to upload")
	}

switch 문

  • 값에 따라서 로직 수행
  • 변수값에 따라 다른 명령을 수행해야 하는 경우
  • if else문을 보기좋게 정리할 수 있음
if day == 1 {
	fmt.Println("첫재 날입니다.")
} else if day ==2 {
	fmt.Println("둘재 날입니다.")
} else {
	fmt.Println("프로젝트를 진행하세요.")
}
switch day {
case 1:
		fmt.Println("첫째 날입니다.")
case 2:
		fmt.Println("둘째 날입니다.")
default:
		fmt.Println("프로젝트를 진행하세요.")
	}

조건문 비교

switch true {
case temp < 10, temp > 30: // -> temp < 10 || temp > 30
	fmt.println("바깥 활동하기 좋은 날씨가 아닙니다.")
case temp >=10 && temp < 20:
	fmt.println("약간 추우니 가벼운 겉옷을 준비하세요.")
default:
	fmt.println("따뜻합니다.")
  • &&, || 의 경우 → 앞의 조건이 부합되면 그 뒤의 조건은 검사하지 않는다
  • → 그러면 좀더 넓은 개념의 조건을 앞에 두어 검사를 최소한 하도록 하는 것이 좋을까?

switch 초기문

  1. 결과값 비교
func getMyAge() int {
	return 22
}

// 결과값 비교
func main() {
	switch age := getMyAge(); age { // getMyAge()가 실행되어서 age 초기화
	case 10:
		fmt.Println("Teenage")
	case 33:
		fmt.Println("Pair 3")
	default:
		fmt.Println("My age is", age)
	}
	
	fmt.Println("age is", age) // Error - age 변수는 사라짐.
}
  • age 는 switch 문에서 선언된 변수이므로 switch문이 종료되기 전까지 접근 가능.
  • switch문이 종료되면 age 변수 사라짐.
  1. 비교값 true
func main() {
	switch age := getMyAge(); true {
	case age < 10:
		fmt.Println("Chile")
	case age < 20:
		fmt.Println("Teenager")
	case age <30:
		fmt.Println("20s")
	default:
		fmt.Println("My age is", age)
	}
}
  • switch 의 비교값으로 true를 사용해서 case 조건문이 true가 되는 경우를 찾는다
  • ture 생략 가능

2022.01.17

배열

  • 배열은 연속된 메모리
    • 배열을 선언하면 컴퓨터는 연속된 메모리 공간을 확보 → 할당
  • 컴퓨터는 인덱스와 타입 크기를 사용해서 메모리 주소를 찾는다
    • 컴퓨터가 인덱스에 해당하는 요소를 찾아가는 방법: 배열의 시작 주소에 ‘인덱스 X 타입크기’를 더해서 찾아간다

다중배열

초기화: 닫는 중괄호가 마지막 요소와 같은 줄에 있지 않은 경우 마지막 항목 뒤에 쉼표를 찍어줘야 한다

a ;= [2][5]int{
	{1, 2, 3, 4, 5},
	{5, 6, 7, 8, 9} } // 쉼표 없음

b ;= [2][5]int{
	{1, 2, 3, 4, 5},
	{5, 6, 7, 8, 9},  // 쉼표 있음
}

구조체

선언

type 타입명 struct {
	필드명 타입
	...
	필드명 타입
}
  • type 키워드를 적어 새로운 사용자 정의 타입을 정의할 것을 공지
  • 타입명의 첫번째 글자가 대문자 → 패키지 외부로 공개되는 타입

2022.02.01

구조체 값 복사

type Student struct {
	Age int // 1️⃣ 대문자로 시작하는 필드는 외부로 공개
	No int
	Score float64
}
							// 함수내 s 인수
func PrintStudent(s student) {
	fmt.Print("나이:%d 번호:%d 점수:%.2f/n", s.Age, s.No, s.Score)
}

func main() {
var student = Student{15, 23, 88.2}

// 2️⃣ student 구조체 모든 필드가 student2로 복사된다
student2 := student

// 3️⃣ 함수 호출 시에도 구조체가 복사된다
PrintStudent(student2)

1️⃣  필드명이 대문자로 시작하는 경우 패키지 외부로 공개되는 필드

2️⃣  student의 모든 필드이 student2fh 복사된다. Age, No, Score의 모든 필드값!

  • 필드값 각각 복사가 아닌 구조체 전체를 한 번에 복사
  • 대입연산자가 우변 값을 좌변 메모리 공간에 복사할 때 ‘복사되는 크기’ = ‘타입크기’
  • 구조체 크기는 모든 필드를 포함하므로 구조체 전체 필드가 복사되는 것

3️⃣  PrintStudent() 함수는 Student 타입을 인수로 받기 때문에 → student2의 모든 필드값이 PrintStudent()

함수 내 s 인수로 복사

2023.02.06

메모리 정렬

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

type User struct {
	Age int32
	Score float64
}

func main() {
	user := User{23, 77.2}
	fmt.Println(unsafe.Sizeof(user)) // 해당 변수에 메모리 공간 크기 반환
}

// 16

int의 크기는 8바이트, int32는 4바이트

Age는 4바이트, Score는 8바이트

💡 왜 User의 크기는 12바이트가 아니라, 16바이트 일까?

User 구조체의 변수 user의 시작 주소가 240번지이면 → Age의 시작주소도 240번지

Age는 4바이트 공간을 차지, 바로 붙여서 Score를 할당하면 Score 주소는 244

그러나, 244는 8의 배수가 아니므로 성능 손해 → Scroe 시작주소는 248

→ 4바이트 띄어져 할당하므로 4바이트 손해

💡 메모리 낭비를 줄이기 위해서는 8바이트 보다 작은 필드는 몰아서 배치하는 것이 효율 적이다!

구조체의 역할

  • 함수는 관련 코드 블록을 묶어서 응집도를 높이고 재사용성을 증가시킴
  • 배열은 같은 타입의 데이터를 묶어서 응집도를 높임
  • 구조체는 관련된 데이터들을 묶어서 응집도를 높이고 재사용성을 증가시킴

결합도(Coupling): 모듈간 상호 의존 관계를 형성해서 서로 강하게 연결되어 있는 정도

응집도(Cohesion): 모듈의 완성도, 모듈 내부의 모든 기능이 단일 목적에 충실하게 모여있는 정도

포인터

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

*: 메모리 주소를 가리키는 포인터 변수( 변수 type 선언 시 사용)

&: 메모리 주소값을 가리키는 기호 (값!!!)

var a int
var p *int

p = &a // a의 메모리 주소를 포인터 변수 p에 대입

*p = 20 // p를 이용하여 변수 a 값을 변경시킴
  • 를 사용하여 변수가 가리키는 메모리 공간에 접근

포인터의 기본값 nil

  • 포인터 변숫값을 초기화하지 않으면 기본값은 nil
  • nil → 어떤 메모리 공간도 가리키고 있지 않다
var p *int
if p != nil {
 // p가 nil이 아니다 -> p가 유효한 메모리 주소를 가리킨다
}

포인터를 쓰는 이유

  • 변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 사용
  • 큰 메모리 공간을 복사할 때도 성능 다운
  • 다른 공간으로 복사되는 것이기 때문에 변경사항이 적용되지도 않음
type Data struct {
	value int
	data [200]int
}

func ChangeData(arg *Data) { // 매개변수로 Data 포인터를 받음 -> Data의 주소값을 받음(data형식)
	arg.value = 999  
	arg.data = 999   // arg 데이터를 변경
}

func main() {
	var data Data

	ChangeData(&data) // 인수로 data의 주소를 넘긴다(값)
}

value = 999

data[100] = 999

→ data의 주소를 인수로 전달

→ 1608바이트의 구조체 전부가 복사되는게 아니라 메모리 주소인 8바이트만 복사됨

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

<기존 방식>

<구조체를 생성해 초기화 하는 방식>

// Data타입 구조체 변수 data 선언
var data Data
var p *Data = &data
// data 변수의 주소값 반환
// *Data 타입 구조체 변수 p 선언
var p *Data = &Data{}
// Data 구조체를 만들어 주소 반환

→ 포인터 변수 p만 가지고도 구조체의 필드값에 접근하고 변경할 수 있다

코드 예시(예전에 짰던 코드)

// CheckFollowing 회원이 팔로우한 유저인지 확인
func (u *userRepo) CheckFollowing(ctx context.Context, userId string, targetId string) (*entity.Follow, error) {

	var follow entity.Follow

	err := u.db.WithContext(ctx).Where("user_id = ? and target_id = ?", userId, targetId).
		First(&follow).Error
	if err != nil {
		return nil, err
	}
	return &follow, err
}

변수를 새로 정의함 → 메모리 공간 소비

// CheckFollowing 회원이 팔로우한 유저인지 확인
func (u *userRepo) CheckFollowing(ctx context.Context, userId string, targetId string) (*entity.Follow, error) {

	var follow *entity.Follow

	err := u.db.WithContext(ctx).Where("user_id = ? and target_id = ?", userId, targetId).
		First(&follow).Error
	if err != nil {
		return nil, err
	}
	return follow, err
}

기존 할당된 entity.Follow 변수를 가리키도록 함 → 메모리 세이브

인스턴스

메모리에 존재하는 데이터의 실체

var p1 *Data = &Data{}
var p2 *Data = p1
var p3 *Data = p1

→ 포인터 변수 3개가 모두 한개의 Data 인스턴스를 가리킨다

→ 포인터를 이용하여 인스턴스에 접근할 수 있다!

→ 구조체 포인터를 함수 매개변수로 받는 다는 것 → 구조체 인스턴스로 입력을 받겠다

2023.02.07

문자열

[]rune타입

  • rune 타입은 int32 타입의 별칭
  • rune의 슬라이스 타입인 rune[]은 상호 타입 변환이 가능
// 1. 문자열의 길이를 알 수 있는 방법
func main() {
	str := "Hello 월드"
	runes := []rune(str) // string 타입 -> rune타입 : 각 글자들로 이뤄진 배열로 변환
	
	fmt.Pringf("len(runes) = %d\n", len(runes))
	// len(unes) = 8
  • rune[]타입으로 변환해서 한글자씩 순회하는 방법도 있고, (배열 할당되므로 메모리 소진)
  • 한 글자씩 순회는 range str {} 로 하는 것이 바로 순회하므로 배열 할당 x

string 구조

// 문자열의 구조
type StringHeader struct {
	Data uinptr 
	Len int
} 
  • string은 필드가 2개인 구조체
  • Data: unitptr 타입으로 문자열의 데이터가 있는 메모리 주소를 나타내는 일종의 포인터
  • Len: int 타입으로 문자열의 길이를 나타냄

2023.02.14

func main() {
	str1 := "안녕하세요"
	str2 := str1
// "안녕하세요"
// "안녕하세요"

str1 문자열을 하나 복사해서 str2가 가리키게 하는 것이 아니라!

구조체 변수는 복사될 때 구조체 크기만큼 메모리가 복사되는데, 즉 Data포인터 값과 Len값이 복사된다.

→ 모두 같은 메모리 데이터를 가리키게 된다

→ 문자열 데이터가 복사되지 않으므로 메모리 성능 문제는 없다.

문자열은 불변이다.

func main() {
	var str string = "Hello"
	str[2] = 'a' // -> Error!! 
	
	var slice []byte = []byte(str)
	slice[2] ='a'

	fmt.Println(str) // Hello
	fmt.Printf("%s\n", slice) // Healo
	
  • 불변이다! → 문자열의 일부만 변경할 수 없다.
  • byte로 타입변환만 했다고 같은 메모리 공간을 가지지 않는다.
  • str는 여전히 Hello 이고 slice는 Healo로 바뀌었다 → str과 slice가 가리키는 메모리 공간은 다르기 때문

→ 슬라이스 타입으로 변환할 때 문자열을 복사해서 새로운 공간을 만들어 슬라이스가 가리키도록 한다

→ 그래야 불변의 원칙을 지킬 수 있기 때문

  • 문자를 합치는 경우에도 새로운 메모리 공간을 만들어서 두 문자열을 합친다.

→ 메모리 낭비를 줄이기 위해 strings.Builder를 사용하면 메모리를 새로 생성하지 않고 기존 메모리 공간에 빈자리가 있으면 더하게 하면 된다.

Pakage

Pakage 사용하기

main pakage

  • 시작점을 포함한 패키지
  • 프로그램 실행 → 운영체제는 프로그램을 메모리에 올림(Load) → main()함수부터 한줄씩 코드 실행

사용하지 않는 pakage

  • 패키지를 import 하고 사용하지 않으면 에러
  • 패키지를 직접 사용하지 않지만 부가효과를 얻고자 import 하는 경우 밑줄 _
import (
		_ "github.com/mattn/go-sqlite3"

Go 모듈

Go 패키지 ⊂ Go 모듈

Go build를 하려면 반드시 Go 모듈 루트 폴더에 go.mod 파일이 있어야 한다.

Go mod tidy : Go 모듈에 필요한 패키지를 찾아서 다운해주고 필요한 패키지 정보를 go.mod파일과 go.sum 파일에 적어준다.

공개여부

  • 외부로 공개: 패키지명, 변수 대문자로 시작 / 비공개: 패키지명 변수 소문자로 시작

2023.02.21

슬라이스

배열: 정한 크기에서 더 이상 늘어나지 않는다.

슬라이스: 동적 배열 → 자동으로 배열 크기를 증가시키는 자료구조

초기화 방법

{}를 이용한 초기화

var slice1 = []int{1,2,3}
var slice2 = []int{1, 5:2. 10:3} // [ 1 0 0 0 0 2 0 0 0 0 3 ]

//배열선언
var array = [...]int{1,2,3}

대괄호 안에 길이를 넣지 않는다.

make()를 이용한 초기화

var slice = make([]int, 3)

slice 변수는 3개짜리 int 슬라이스 값을 갖는다.

각 슬라이스 요솟값은 int 타입의 기본값인 0

배열과 슬라이스의 동작 차이

Go 언어에서 모든 값의 대입복사로 일어난다. → 함수에서 인수로 전달될 때, 다른 변수에 대입할 때

포인터는 포인터의 값인 메모리 주소가 복사되고, 구조체가 복사될 때는 구조체의 모든 필드 복사

배열은 배열의 모든값복사된다.

func changeArray(array2 [5]int) {
	array2[2] = 200
}

func changeSlice(slice2 []int) {
	slice2[2] = 200
}

func main() {
	array := [5]int{1, 2, 3, 4, 5}
	slice := []int{1, 2, 3, 4, 5}

changeArray(array) // [1 2 3 4 5]
changeSlice(slice) // [1 2 200 4 5]

→ slice 의 3번째 값은 변했지만 array의 3번째 값은 변하지 않았다.

array 타입은 [5]int로 크기는 40바이트(8x5 = 40)

  • changeArray()함수로 입력후 호출 시 → array값이 array2로 복사 → 40바이트 복사
    • array 배열의 모든 값이 array2로 복사 → array, array2 는 메모리 공간이 다른 완전히 다른 배열
    • 따라서, array2의 3번째 값을 바꿔도 array의 3번째 값은 변하지 않는다

slice 타입은 []int → 내부는 포인터, len, cap 세개의 필드를 갖는 구조체 이다.

포인터, len, cap 각각 8바이트로 총 24바이트

  • changeSlice()함수 호출 시 → slice 복사 시 주솟값도 복사 → slice, slice2는 같은 배열

빈공간이 없을 때, append() 사용 시 발생하는 일

slice1 := []int{1, 2, 3}
slice2 := append(slice1, 4, 5}

slice[1] = 100
// slice1 = {1, 100, 3}
// slice2 = {1, 2, 3, 4, 5, ⭕️}

append()함수가 호출되면 먼저 빈공간이 충분한지 확인한다.

만약 빈공간이 충분하지 않다면, 일반적으로 기존 배열의 2배 크기를 마련한다 → 새로운 배열을 만든다

  • slice1 배열의 두 번째 값을 변화시켜도 slice2 배열은 값이 바뀌지 않는다 → 서로 다른 배열

슬라이싱

배열의 일부를 집어내는 기능으로 결과물도 슬라이스이다.

array[시작인덱스:끝인덱스] → 시작인덱스부터 끝인덱스-1까지의 배열 일부를 슬라이싱한다.

array := [5]int{1, 2, 3, 4, 5}
slice := array[1:2]

array[1] = 100 // slice=100 -> slice의 포인터가 array의 2인덱스를 가리키기 때문이다.
  • 슬라이싱은 배열의 일부를 잘라 새로운 배열을 만드는 것이 아니라, slice 의 포인터가 배열의 인덱스를 가리킨다.
  • 슬라이스 슬라이싱도 가능하다.

슬라이스 복제

slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // slice1과 같은 길이의 슬라이스 생성

// 방법1
for i, v := range slice1 {
	slice2[i] = v
}
slice[1] = 100
// slice1 -> [1, 100, 3, 4, 5]
// slice2 -> [1, 2, 3, 4, 5]

// 방법2
slice2 := append([]int{}, slice1...)

//방법3
new := copy(slice2, slice1) // slice1을 slice2에 복사
  • 방법1-반복문
    • slice1과 똑같은 길이의 다른 슬라이스를 만든다
    • slice1의 모든 요솟값을 하나씩 slice2로 복사한다
  • 방법2-append()
    • slice1의 모든 값을 복제한 새로운 슬라이스를 만들어서 slice2에 대입
    • 배열이나 슬라이스 뒤에 … 를 하면 모든 요솟값을 넣어주는 것
    • slice2 := append([]int{}, slice1[0], slice1[1], slice1[2] …) 와 같다
  • 방법3-copy()
    • func copy(dst, src []Type) int
    • 첫번째 인수는 복사한 결과를 저장하는 슬라이스 변수, 두 번째 인수는 복사 대상이 되는 슬라이스 변수
    • 두 길이가 다르다면 길이 중 작은 개수만큼만 복사됨
profile
문제를 해결하는 백엔드 개발자

2개의 댓글

comment-user-thumbnail
2023년 8월 2일

많은 도움이 되었습니다, 감사합니다.

1개의 답글