Study

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

mokpolar 2023. 12. 3. 16:52
반응형

21장 함수 고급편

21.1 가변 인수 함수

  • 인수 개수 가 정해져 있지 않은 함수
fmt.Println(1, 2, 3, 4, 5, 6..) // 인수가 많을 수 있다. 

21.1.1 … 키워드 사용

  • … 키워드를 사용해서 가변 인수를 처리할 수 있다.
  • 인수 타입 앞에 …를 붙여서 해당 타입 인수를 여러 개 받는 가변인수 라는 걸 표시하면 된다.
package main

import "fmt"

func sum(nums ...int) int { // 가변 인수를 받는 함수
	sum := 0

	fmt.Printf("nums 타입 : %T\\n", nums) // nums 타입 출력

	for _, v := range nums {
		sum += v
	}
	return sum
}

func main() {
	fmt.Println(sum(1, 2, 3, 4, 5, 6)) 
}
  • 위와 같이 … 로 가변인수를 표시하고 함수 내부에서는 슬라이스 타입으로 동작한다.
  • 아래와 같이 빈 인터페이스를 통해 모든 타입의 가변인수를 받을 수 있다.
func Print(args ...interface{}) { // 모든 타입을 받는 가변 인수
	for _, arg := range args { // 모든 인수 순회
		switch f := arg.(type) { // 인수의 타입에 따른 동작
		case bool:
			val := arg.(bool) // 인터페이스 변환
		case float64:
			val := arg.(float64)
		case int:
			val := arg.(int)
		}
	}
}

21.2 defer 지연 실행

  • 파일이나 소켓 핸들 처럼 OS 내부 자원을 사용하는 경우
  • 함수가 종료되기 직전에 실행해야 하는 코드가 있을 수 있다.
  • 파일을 생성하거나 읽을 때 OS에 파일 핸들을 요청한다.
  • 그러면 OS는 파일 핸들을 만들어서 프로그램에 알려준다.
  • 이는 OS 내부 자원이기 때문에 쓰고나서 OS에 되돌려줘야 한다.
  • 이렇게 함수 종료 전에 처리해야 하는 코드가 있을때 defer를 사용한다.
  • defer를 사용하면 명령문이 바로 실행되지 않고, 해당 함수가 종료되기 직전에 실행되도록 지연된다.
defer 명령문

21.3 함수 타입 변수

  • 함수 타입 변수 : 함수를 값으로 갖는 변수
  • 포인터 처럼 함수를 가리킨다고 해서 함수 포인 function pointer 라고 부른다.
func add(a, b int) int {
	return a + b
}
// 위 함수를 가리키는 함수 포인터는 아래와 같이 표현한다. 
func (int, int) int
package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func mul(a, b int) int {
	return a * b
}

func getOperator(op string) func(int, int) int { // op에 따른 함수 타입 반환
	if op == "+" {
		return add
	} else if op == "*" {
		return mul
	} else {
		return nil
	}
}

func main() {
	var operator func(int, int) int
	operator = getOperator("*")

	var result = operator(3, 4)
	fmt.Println(result)
}

// 12
  • 아래를 보면 func (int, int) int 부분이 함수 타입 정의이다.
  • int 타입 인수 2개를 받고 int 타입을 반환하는 함수 타입을 나타낸다.
func getOpeartor(op string) func (int, int) int

 

 💡 별칭으로 함수 정의 줄여 쓰기 :

함수정의는 길기 때문에 별칭 타입을 써서 함수 정의를 짧게 줄일 수 있다.

type opFunc func (int, int) int

func getOpeartor(op string) opFunc

 

💡 매개 변수명 생략 하기 :

func (int, int) int func (a int, b int) int

 

21.4 함수 리터럴

  • 함수 리터럴 function literal : 이름 없는 함수로 함수명을 적지 않고, 함수 타입 변숫값으로 대입되는 함숫값
  • 함수명이 없기 때문에 함수명으로 직접 함수를 호출 할 수 없고, 함수 타입변수로만 호출된다.
  • 익명함수 람다 lambda , 함수 리터럴 으로 부른다.
package main

import "fmt"

type opFunc func(a, b int) int

func getOperator(op string) opFunc {
	if op == "+" {
		return func(a, b int) int {
			return a + b // 함수 리터럴을 사용해서 더하기 함수를 정의하고 반환
		}
	} else if op == "*" {
		return func(a, b int) int {
			return a * b
		}
	} else {
		return nil
	}
}

func main() {
	fn := getOperator("*")

	result := fn(3, 4) // 함수 타입 변수를 사용해서 함수 호출 

	fmt.Println(result)
}

// 12

21.4.1 함수 리터럴 내부 상태

  • 함수 리터럴 내부에서 사용되는 외부 변수는 자동으로 함수 내부 상태로 저장된다.

21.4.2 함수 리터럴 내부 상태 주의점

  • 함수 리터럴 외부 변수를 내부 상태로 갖고 오는 것을 캡쳐 capture 라고 한다.
  • 캡쳐는 값 복사가 아니라 참조 형태로 가져온다.

22장. 자료 구조

22.1 리스트

  • 배열과 리스트의 차이점
    • 배열 : 연속된 메모리에 데이터를 저장한다.
    • 리스트 : 불연속된 메모리에 데이터를 저장한다.

22.1.1 포인터로 연결된 요소

  • 리스트는 각 데이터를 담고 있는 요소들을 포인터로 연결한 자료구조 이다.
  • 링크드 리스트 라고도 부른다.
type Element struct { // 구조체
	Value interface{} // 데이터를 저장하는 필드
	Next *Element // 다음 요소의 주소를 저장하는 필드
	Prev *Element // 이전 요소의 주소를 저장하는 필드
}
  • Value는 실제 요소의 데이터를 저장하며 interface {} 타입이므로 어떤 타입값도 저장할 수 있다.
  • Prev가 있으므로 양방향 리스트이다.

22.1.2 리스트 기본 사용법

package main

import (
	"container/list"
	"fmt"
)

func main() {
	v := list.New()       // 새로운 리스트 생성
	e4 := v.PushBack(4)   // 리스트 뒤에 요소 추가
	e1 := v.PushFront(1)  // 리스트 앞에 요소 추가
	v.InsertBefore(3, e4) // e4요소 앞에 요소 삽입
	v.InsertAfter(2, e1)  // e1요소 뒤에 요소 삽입

	for e := v.Front(); e != nil; e = e.Next() {
		//각 요소 순회
		fmt.Print(e.Value, " ")
	}

	fmt.Println()

	for e := v.Back(); e != nil; e = e.Prev() {
		// 각 요소 역 순회
		fmt.Print(e.Value, " ")
	}
}

// 1 2 3 4 
// 4 3 2 1

22.1.3 배열 vs 리스트

맨 앞에 데이터를 추가할 때 배열 VS 리스트

  • 배열의 경우
  • 한 칸씩 뒤로 밀어야 하기 때문에 Big-O 표기법으로 O(N) 알고리즘

  • 리스트의 경우
  • 각 요소를 밀어낼 필요없이 맨 앞에 요소를 추가하고 연결을 만들어주면 된다.
  • Big-O 표기법으로 O(1), 상수시간이 걸린다.

특정 요소에 접근하기

  • 배열의 경우
    • 네 번째 요소에 접근하려면
    • 배열 시작 주소 + (인덱스 * 타입 크기)
    • 요소개수와 상관없이 상수 시간이 걸리기 때문에 Big-O로 O(1)
  • 리스트의 경우
    • 각 요소가 포인터로 연결되어 있기 때문에 앞 요소를 모두 거쳐야 네 번째 요소에 접근 가능
    • 특정 요소에 접근하려면 N-1번 링크를 타야해서 Big-O 표기법으로 O(N)만큼 시간이 걸린다고 표현한다.

22.1.4 실습 : 큐 구현하기

  • 리스트로 큐 만들기
    • 들어간 순서 그대로 빠져나오기 때문에 순서가 유지
    • 새로운 요소는 항상 맨 마지막에 추가
    • 출력값은 맨 앞에서 하나씩 빼내게 된다.
  • 배열로 큐를 만들면 맨 앞에서 출력값이 발생하기 때문에 배열로 만들면 요소를 빼낼 때 마다 O(N)이 필요
  • 리스트로 만들면 O(1) 성능을 보장하므로 리스트가 더 효율적

package main

import (
	"container/list"
	"fmt"
)

type Queue struct {
	v *list.List // Queue 구조체 정의
}

func (q *Queue) Push(val interface{}) {
	q.v.PushBack(val) // 맨 뒤에 요소 추가 , 빈 인터페이스로 모든 타입의 데이터 저장
}

func (q *Queue) Pop() interface{} { // 요소를 반환하면서 삭제
	front := q.v.Front() // Front()는 맨 앞의 요소 인스턴스를 반환
	if front != nil {
		return q.v.Remove(front) // 비지 않았다면 Remove() 메서드 호출. 요소 삭제후 반환. 
	}
	return nil // 리스트가 비었으면 nil 반환
}

func NewQueue() *Queue { // 새로운 인스턴스
	return &Queue{list.New()} // 내부 리스트 필드도 list.New() 함수를 이용해서 같이 초기화
}

func main() {
	queue := NewQueue() // 새로운 큐 생성

	for i := 1; i < 5; i++ { // for문을 이용해서 큐에 요소 추가
		queue.Push(i)
	}
	v := queue.Pop() //
	for v != nil {
		fmt.Printf("%v -> ", v)
		v = queue.Pop() // 리스트가 비어있지 않다면 Pop() 결과가 nil이 아니기 때문에 모두 빌때까지
	}
}

// 1 -> 2 -> 3 -> 4 ->

22.1.5 실습 : 스택 구현하기

  • Stack은 FILO
    • 가장 최근에 넣은 것부터 역순으로 나오게 된다
    • 요소는 맨 뒤로 추가한다
    • 요소를 뺄 때도 맨 뒤에서 빼낸다.
  • 순서가 반대가 되기 때문에 스택은 가장 최신 것부터 하나씩 되돌릴 때 주로 사용한다.

package main

import (
	"container/list"
	"fmt"
)

type Stack struct {
	v *list.List
}

func NewStack() *Stack {
	return &Stack{list.New()}
}

func (s *Stack) Push(val interface{}) {
	s.v.PushBack(val) // 맨 뒤에 요소 추가
}

func (s *Stack) Pop() interface{} {
	back := s.v.Back() // 맨 뒤에서 요소 반환
	if back != nil {
		return s.v.Remove(back)
	}
	return nil
}

func main() {
	stack := NewStack()

	for i := 1; i < 5; i++ {
		stack.Push(i)
	}

	val := stack.Pop()
	for val != nil {
		fmt.Printf("%v -> ", val)
		val = stack.Pop()
	}
}

// 4 -> 3 -> 2 -> 1 ->

22.2 링

  • 맨 뒤의 요소와 맨 앞의 요소가 서로 연결된 자료구조이다

package main

import (
	"container/ring"
	"fmt"
)

func main() {
	r := ring.New(5) // 요소가 5개인 링 생성

	n := r.Len() // 링 길이 반환

	for i := 0; i < n; i++ {
		r.Value = 'A' + i // 순회하면 모든 요소에 값 대입, 각 요솟값을 알파벳 A부터 E까지 설정
		r = r.Next()
	}

	for j := 0; j < n; j++ {
		fmt.Printf("%c ", r.Value) // 순회하면서 값 출력
		r = r.Next()
	}

	fmt.Println() // 한 줄 띄우기

	for j := 0; j < n; j++ {
		fmt.Printf("%c ", r.Value) // 역순하면 값 출력
		r = r.Prev()
	}
}

// A B C D E
// A E D C B

 

22.2.1 링은 언제 쓸까?

  • 링은 저장할 개수가 고정되고, 오래된 요소는 지워도 되는 경우에 적합하다.
  • ctrl+Z
  1. 실행 취소 기능 : 문서 편집 기능 등에서 일정한 개수의 명령을 저장하고 실행 취소하는 경우
  2. 고정 크기 버퍼 기능 : 데이터에 따라 버퍼가 증가되지 않고 고정된 길이로 쓸 때
  3. 리플레이 기능 : 게임 등에서 최근 플레이 10초를 다시 리플레이 할 때

22.3 맵

  • 키와 값 형태로 데이터를 저장하는 자료구조이다.
  • 딕셔너리, 해시테이블, 해시맵
m := make(map[string]string) // 맵 생성
m["이화랑"] = "서울시 광진구" // 키와 값 추가

22.3.1 요소 삭제와 없는 요소 확인

  • delete() 함수로 요소를 삭제한다.
delete(m, key) // 맵 변수와 삭제 키 
  • 값이 0일때와 아예 요소가 없을 때 모두 0을 출력한다.

22.3.2 맵, 배열, 리스트 속도 비교

  • 맵은 속도가 빠르고, 삭제, 추가, 읽기에서 요소 개수와 상관없이 속도가 일정하다.
  • 배열은 추가, 삭제해서 요소 개수가 많아질수록 오래걸린다.
  • 리스트는 요소 읽기에서 요소 개수가 많아질수록 오래 걸린다.
  • 맵은 인덱스를 사용해서 접근할 수 없고 입력순서가 보장되지 않으며 메모리를 많이 차지한다.

22.4 맵의 원리

22.4.1 해시 함수

  • 해시 함수의 특징
    1. 같은 입력이 들어오면 같은 결과가 나온다
    2. 다른 입력이 들어오면 되도록 다른 결과가 나온다
    3. 입력값의 범위는 무한대이고 결과는 특정 범위를 갖는다.

  • 나머지 연산도 자주 쓰인다.

22.4.2 해시로 맵을 만들자

const M = 10
func hash(d int) int {
	return d % M
}

var m [M]int // M은 10

m[hash(23)] = 10
  • 해시 함수 hash(23)의 결과는 23%10인 3이 반환되므로 인덱스가 3인 위치에 값 10을 대입한다.

  • 해시 충돌이 일어날 때는 인덱스 위치에 값이 아니라 리스트를 저장하면 된다.
  • 같은 인덱스에 위치하지만 다른 값을 갖는다

 

반응형