반응형
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
- 실행 취소 기능 : 문서 편집 기능 등에서 일정한 개수의 명령을 저장하고 실행 취소하는 경우
- 고정 크기 버퍼 기능 : 데이터에 따라 버퍼가 증가되지 않고 고정된 길이로 쓸 때
- 리플레이 기능 : 게임 등에서 최근 플레이 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 해시 함수
- 해시 함수의 특징
- 같은 입력이 들어오면 같은 결과가 나온다
- 다른 입력이 들어오면 되도록 다른 결과가 나온다
- 입력값의 범위는 무한대이고 결과는 특정 범위를 갖는다.
- 나머지 연산도 자주 쓰인다.
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을 대입한다.
- 해시 충돌이 일어날 때는 인덱스 위치에 값이 아니라 리스트를 저장하면 된다.
- 같은 인덱스에 위치하지만 다른 값을 갖는다
반응형
'Study' 카테고리의 다른 글
[스터디] Tucker의 Go 언어 프로그래밍 내용정리 24~26장 (0) | 2023.12.12 |
---|---|
[스터디] Tucker의 Go 언어 프로그래밍 내용정리 18~20장 (0) | 2023.11.24 |
[스터디] Tucker의 Go 언어 프로그래밍 내용정리 14~17장 (1) | 2023.11.18 |
[스터디] PKOS 스터디 1주차 Kops 개념 및 실습 (0) | 2023.03.11 |
[스터디] Terraform module을 이용한 여러 EKS 배포 (0) | 2022.12.10 |