사용 교재 : 가장 빨리 만나는 Go언어
3주차 : UNIT 24 ~ 34
[UNIT 24] 함수 사용하기
- 함수 정의 & 호출
> 함수 정의를 시작한 줄에서 {(여는 중괄호)가 시작되어야 한다
> Go언어에서 함수를 정의할 때 위치의 제약이 없다 (다른 언어는 함수 호출보다 함수 선언 or 정의가 앞에 있어야 함)
1. 매개변수와 리턴값 사용하기
- 형식 : func 함수명(매개변수명 자료형)리턴값_자료형 { }
- 리턴값에 이름을 지정할 수 있다
- 리턴값 변수에 rktq을 대입한 뒤 마지막에 return을 사용할 때 뒤에 리턴할 변수를 지정하지 않아도 된다
2. 리턴값 여러 개 사용하기
- Go언어에서는 여러 개의 값을 리턴할 수 있다
- 형식 : func 함수명(매개변수명 자료형)(리턴값_자료형1, 리턴값_자료형2) { }
> 만약 두 번째 리턴값만 사용하고 싶다면 _(밑줄 문자)를 사용한다
> 만약 _(밑줄 문자)를 사용하지 않으면 에러 발생
> 리턴값 여러 개 가운데 특정 값만 생략하고 사용할 수 있다 (_(밑줄 문자) 사용)
- 값을 여러 개 리턴할 때도 리턴값에 이름을 정할 수 있다
- 형식 : func 함수명(매개변수명 자료형)(리턴값_변수명1 자료형, 리턴값_변수명2 자료형) { }
3. 가변인자 사용하기
- 가변인자 : 함수의 매개변수 개수가 정해져 있지 않고 유동적으로 변하는 형태
- 형식 : func 함수명(매개변수명 …자료형)리턴값_자료형 { }
* 가변인자는 슬라이스 타입이므로 range 키워드로 값을 꺼내 사용한다
4. 재귀호출 사용하기
- 재귀함수(Recursive function) : 함수에서 자기 자신을 다시 호출하는 함수
* 팩토리얼 : n의 값을 입력했을 때 1 ~ n의 모든 자연수의 곱
5. 함수를 변수에 저장하기
- 함수를 변수에 저장할 수 있다
- var 변수명 func(매개변수명 자료형) 리턴값_자료형 = 함수명
- 슬라이스(배열)에도 함수를 간단하게 저장할 수 있다
- 형식 : 슬라이스 = []func(매개변수명 자료형) 리턴값_자료형{함수명1, 함수명2}
- 맵 역시 함수를 저장할 수 있다
- 형식 : 맵 := map[키_자료형]func(매개변수명 자료형) 리턴값_자료형{“키”: 함수명}
6. 익명 함수 사용하기
- 자바스크립트처럼 Go언어도 함수 안에서 이름이 없는 익명 함수를 정의하고 호출할 수 있다
- 형식 : func(매개변수명 자료형)리턴값_자료형 {}()
> 익명 함수는 함수를 정의한 뒤 ( )(괄호)를 사용해 바로 함수를 호출한다
[UNIT 25] 클로저 사용하기
- 클로저 : 함수 안에서 함수 선언 및 정의하거나 바깥쪽 함수에 선언된 변수에도 접근할 수 있는 함수
바깥 함수가 변수와 자기 자신(함수)을 에워싸고 있는 것
- 함수 안에서 함수를 선언하고 정의
> 익명 함수는 일반적으로 함수를 정의할 때 이름이 없음
- 익명 함수의 바깥 변수를 사용하는 경우
> 익명 함수가 바깥 변수를 사용하는 것이 클로저를 사용하는 것이다
> 클로저를 사용하면 지역 변수가 소멸되지 않고 함수 호출할 때마다 계속 가져다 쓸 수 있다
> 프로그램의 흐름을 변수에 저장할 수 있다
[UNIT 26] 지연 호출 사용하기
- 지연 호출 : 함수를 현재 함수가 끝나기 직전에 실행하는 기능
- try finally 구문과 비슷하게 동작
- defer 함수명()
- defer 함수명(매개변수)
- 함수 안에 익명 함수를 선언 및 정의하고 지연 호출
> 함수 뒤에 ()를 붙이면 함수를 바로 호출하는 것
- 지연 호출한 함수가 실행되는 순서는 자료구조의 스택(LIFO, Last In First Out)과 동일하다
- 맨 나중에 지연 호출한 함수가 먼저 실행된다
- 지연 호출은 파일을 열고 닫을 때 유용하게 활용할 수 있다
> os.Open을 통해 파일을 연고 file.close는 지연 호출로 호출해 함수가 끝날 때 무조건 닫을 수 있도록 한다
> 지연 호출을 사용한 위와 같은 방식은 프로그램 흐름에 분기가 많아 에러 처리가 복잡할 때 유용하다
[UNIT 27] 패닉과 복구 사용하기
- 패닉 : 프로그램이 잘못되어 에러가 발생한 뒤 종료되는 상황
> 배열의 크기보다 큰 인덱스에 접근했을 경우 발생하는 에러
- panic 함수를 사용해 사용자가 직접 에러를 발생시킬 수도 있다
- 문법적인 에러는 아니지만 로직에 따라 에러 처리를 하고 싶을 때 사용한다
- recover 함수를 사용하면 패닉이 발생했을 때 프로그램이 바로 종료되지 않고 예외 처리를 할 수 있다
- try catch 구문과 비슷하게 동작
- defer로 처리해야 패닉 함수가 끝나고 recover함수를 호출해야 한다
- 런타임 에러 상황에서도 recover함수를 사용하면 프로그램이 종료되지 않고 계속 실행된다
[UNIT 28] 포인터 사용하기
- Go언어 역시 메모리 주소를 표현하는 포인터를 지원한다
- 형식 : var 변수명 *자료형
- new 함수로 지정한 자료형의 크기에 맞는 메모리 공간 할당
- 메모리를 관리하는 가비지 컬렉션을 지원하기 때문에 메모리 할당 후 해제하지 않아도 된다
- 포인터 형 변수에 값을 대입하거나 가져오려면 역참조를 사용한다
- 변수를 선언할 때 *를 붙이면 포인터형 변수지만, 변수를 사용할 때 *를 붙이면 역참조가 된다
- 포인터 변수에는 메모리 주소가 저장된다
- 일반 변수에 참조(레퍼런스, &)를 사용하면 포인터형 변수에 대입할 수 있다
- 변수 앞에 &를 붙이면 해당 변수의 메모리 주소를 뜻한다
- 메모리 주소를 직접 대입하거나 포인터 연산을 허용하지 않는다
- 다음의 예시는 불가능
1. 함수에 포인터형 매개변수 사용하기
- 함수에 정수형 값을 넘기고 함수 안에서 매개변수에 다른 값 대입하더라도 함수 바깥 변수에는 영향을 주지 X
- 포인터형 매개변수를 사용하면 값을 바꿨을 때 함수 바깥 변수에 영향을 준다 (메모리 주소를 넘겼기 때문)
[UNIT 29] 구조체 사용하기
- 형식 : type 구조체명 struct { }
- 구조체 인스턴스는 일반 변수를 선언하는 방법과 같다 (var 변수명 구조체_타입)
- 구조체 필드의 자료형은 기본 값으로 초기화 된다 (string은 “”, uint는 0, float32는 0.0)
type Rectangle struct{
width int
height int
}
func main() {
var rect Rectangle
}
- 지역 변수 형태가 아닌 포인터에 메모리 공간을 할당할 수 있음 (구조체_포인터 = new(구조체_타입)
type Rectangle struct {
width int
height int
}
func main() {
var rect *Rectangle
rect1 = new(Rectangle)
rect2 := new(Rectanle)
}
- 구조체 인스턴스는 생성할 때 값을 초기화 할 수 있다 (구조체_인스턴스 = 구조체 타입{ })
- 중괄호 블록 안에 필드 순서대로 값을 나열해 저장할 수 있다
- 필드명을 생략했을 때는 개수를 모두 채워주어야 한다, 필드명을 지정한다면 모두 채우지 않아도 됨
type Rectangle struct{
width int
height int
}
func main() {
var rect1 Rectangle = Rectangle{10, 20}
rect2 := Rectangle{45, 62}
rect3 := Rectangle{width: 30, height: 15}
}
- 구조체 인스턴스의 필드에 접근할 땐 .(점)을 사용
- new 함수로 메모리를 할당한 구조체 인스턴스 필드에 접근할 때 역시 .(점)을 사용함
> 구조체 인스턴스는 출력할 때 앞에 &이 붙음 (주소를 뜻함)
> Print 함수로 구조체 인스턴스나 포인터를 출력하면 필드의 내용이 그대로 출력된다
1. 구조체 생성자 패턴 활용하기
- new 함수로 구조체의 메모리를 할당하는 동시에 값을 초기화하지는 못한다
- 지역 변수 형태로 생성된 구조체나 구조체 포인터를 리턴할 수 있다
- 위 코드와 같은 의미로 줄여서 쓸 수 있다
2. 함수에서 구조체 매개변수 사용하기
- 사각형 넓이 구하기 함수 만들기
- 함수의 매개변수에 구조체 포인터가 아닌 일반적인 형태로 넘겨주면 값이 복사됨을 주의!
> rectangleScaleA는 구조체 포인터를 매개변수로 받아 원래 값이 변경됨
> rectangleScaleB는 구조체를 그대로 받아 값이 복사되기 때문에 원래 값에는 영향을 주지 않는다
[UNIT 30] 구조체에 메서드 연결하기
- go언어에는 클래스가 없는 대신 구조체에 메서드를 연결할 수 있다
- 형식 : func (리시버명 *구조체_타입)함수명() 리턴값_자료형{ }
- 함수를 정의할 때 func 키워드와 함수명 사이에 리시버 부분이 추가됨 (리시버 변수를 통해 인스턴스 값에 접근)
- 구조체 인스턴스에 .(점)을 사용해 메서드를 호출한다
- 리시버로 정의한 변수에는 메서드가 포함된 구조체의 인스턴스 포인터가 들어있다
→ 리시버 변수를 통해 현재 인스턴스 필드 값을 가져오거나 변경한다 (this 포인터와 비슷)
- 함수에 리시버 변수를 받는 방법도 포인터와 일반 구조체 방식이 있다
> scaleA 메서드는 리시버 변수로 구조체 포인터를 받기 때문에 원래 값이 변경됨
> scaleB 메서드는 구조체 그대로 받아 값이 복사되기 때문에 원래의 값에는 영향을 미치지 않는다
- 메서드를 작성할 때 구조체 인스턴스의 값을 변경하면 포인터 형태로, 일반적인 상황에서는 리시버 변수를 값 형태로 받아야 한다
- 리시버 변수를 사용하지 않는다면 _(밑줄 문자)로 변수를 생략할 수 있다
[UNIT 31] 구조체 임베딩 사용하기
- Go는 클래스를 제공하지 않으므로 상속 역시 없음
- 구조체에서 임베딩을 사용하면 상속과 같은 효과를 낼 수 있음
> Student 구조체에 Person 필드가 있어 Has-a 관계가 된다
> 학생 구조체는 사람 구조체를 갖고 있음
> greeting 함수를 호출할 때 s.p.greeting()처럼 p 필드를 통해 호출가능하다
- Student 구조체에 Person 구조체를 임베딩한 경우
> Student 구조체에서 Person 필드를 정의할 때 필드명을 사용하지 않고 타입만 지정하면 구조체가 해당 타입을 포함하는 Is-a 관계가 된다
> greeting 함수를 호출할 때, student에서 Person을 통해 greeting을 호출하는 것처럼 student에서 바로 호출할 수 있다
1. 메서드 오버라이딩 상황
- Student 구조체도 Person 구조체와 같은 이름의 greeting 메서드를 갖고 있다면 Student 구조체의 greeting함수가 오버라이드된다
- 부모 구조체의 메서드 이름과 중복되면 상속 과정의 맨 아래 메서드가 호출됨
[UNIT 32] 인터페이스 사용하기
- 인터페이스는 메서드의 집합
- 형식 : type 인터페이스명 interface { }
- 선언 : var 변수명 인터페이스
-> 빈 인터페이스 정의 & 선언
- 메서드를 갖고 있는 인터페이스 정의
- 형식 : type 인터페이스 interface {메서드}
- { }(중괄호) 블록 안에 메서드 이름, 매개변수 자료형, 리턴값 자료형을 지정해 한 줄씩 나열한다 (,(콤마)로 구분하지 x)
- 서로 다른 자료형 두 개를 한 개의 인터페이스에 담기
- 인터페이스를 선언하면서 초기화하려면 :=를 사용하면 된다
- 인터페이스에는 ( )(괄호)를 사용해 변수나 인스턴스를 넣어준다
- 배열 / 슬라이스 형태로도 인터페이스 초기화 가능
1. 덕 타이핑
- 덕 타이핑 : 값이나 인스턴스의 실제 타입은 상관하지 않고 구현된 메서드로만 판단하는 방식
- 타입이 특정 인터페이스를 구현하는지 검사하려면 다음과 같이 쓴다
- 형식 : interface{ }(인터페이스).(인터페이스)
> Duck 타입의 인스턴스 donald를 빈 인터페이스에 넣고 Quacker 인터페이스와 같은지 확인
> 첫 번째 리턴값은 검사했던 인스턴스
> 두 번째 리턴값은 인스턴스가 해당 인터페이스를 구현하고 있는가 여부
2. 빈 인터페이스 사용하기
- 인터페이스에 아무 메서드도 저장되어 있지 않으면 모든 타입을 저장할 수 있다
func f1(arg interface{}) {
}
- 빈 인터페이스는 다음처럼 Any로 표현할 수 있다
- 빈 인터페이스 타입은 함수의 매개변수, 리턴값, 구조체의 필드로 사용가능
type Any interface{}
func f2(arg Any) {
}
- 모든 타입을 받아 내용을 출력하는 함수
- 일반 자료형뿐만 아니라 구조체 인스턴스 및 포인터도 빈 인터페이스로 넘길 수 있음
- 인터페이스에 저장된 타입이 특정 타입인지 검사
[UNIT 33] 고루틴 사용하기
1. 멀티코어 활용하기
- Go 언어는 CPU 코어를 한 개만 사용하도록 설정되어 있음
- 다음은 시스템의 모든 CPU 코어를 사용하는 방법
* runtime.NumCPU : 현재 시스템의 CPU 코어 개수 구하기
[UNIT 34] 채널 사용하기
> 채널에 값이 들어오면 대기를 끝내고 다음 코드를 실행
> 채널은 값을 주고받는 동시에 동기화 역할 수행
> make를 통해 동기 채널을 생성한다
> 고루틴을 생성해 반복문을 실행할 때마다 채널 done에 true 값을 보내 1초씩 기다린다
> done에 값을 보내면 다른 쪽에서 값을 꺼낼 때까지 대기한다
> <-done에서 채널에 값이 들어올 때까지 대기하고 값을 보내면 값을 꺼내고 다음 코드를 진행한다
> 고루틴 쪽의 대기도 종료되고 다시 반복문이 실행된 뒤 채널에 값을 보낸다
> 메인 함수는 채널에서 값을 꺼내고 다시 고루틴도 채널에 값을 보낸다
> 고루틴 → 메인 함수 → 고루틴 → 메인 함수
2. 채널 버퍼링
- 채널의 버퍼가 가득 차면 값을 꺼내 출력하는 코드
> 고루틴을 생성해, 반복문을 실행할 때마다 채널 done에 true 라는 값을 보낸다
> 채널의 버퍼를 2개로 설정했기 때문에 done에 true를 2번 보내고 다음 루프에서 대기한다
> 메인 함수에서는 반복문을 실행할 때마다 채널에서 done 값을 꺼내온다
> 비동기 채널에 버퍼가 2개이므로 done에는 이미 2개의 값이 들어있으므로 루프를 두 번 반복하며 <-done에서 값을 꺼낸다
> 고루틴 쪽에서 값을 두 번 보내고, 메인 함수에서 두 번 꺼낸다
> 고루틴 → 고루틴 → 메인 함수 → 메인 함수
3. range와 close 사용하기
- 0부터 4까지 채널에 값을 보내고 다시 채널에서 값을 꺼내 출력
> for 반복문 안에서 range 키워드를 사용해 채널이 닫힐 때까지 반복해 값을 꺼낸다
> 동시에 고루틴 안에서 c에 0부터 4까지 값을 보내고 close로 채널을 닫는다
> range로 0부터 4까지 꺼내고, 값을 출력한 뒤 반복문이 종료된다
< range와 close 함수의 특징 >
- 이미 닫힌 채널에 값을 보내면 패닉이 발생
- 채널을 닫으면 range 루프가 종료됨
- 채널이 열려 있고, 값이 들어오지 않으면 range는 실행되지 않고 계속 대기한디. 만약 다른 곳에서 채널에 값을 보내면 그때부터 range가 계속 반복
- 채널을 가져와 두 번째 리턴값으로 채널이 닫혔는지 확인하는 코드
> 두 번째 매개변수가 true이면 채널이 열린 상태, false면 채널이 닫힌 상태
4. 보내기 전용 및 받기 전용 채널 사용하기
- 보내기 전용 채널과 받기 전용 채널은 값의 흐름이 한 방향으로 고정되어 있다
- 0부터 4까지 채널에 값을 보내고, 다시 채널에서 값을 꺼내 출력한다. 긜고 반복 문이 끝난 뒤 채널에 100을 보내 다시 출력하는 코드
> 보내기 전용 : chan <- 자료형 형식
값을 보낼 수만 있으며 값을 가져오려고 하면 컴파일 에러가 발생
> 받기 전용 : <- chan 자료형 형식
range 키워드 또는 <-채널 형식으로 값을 꺼낼 수만 있고 값을 보내려고 하면 컴파일 에러 발생
- 채널을 치턴값으로 사용 (두 수를 더한 뒤 채널로 리턴)
- 채널만 사용해 값을 더하는 코드
> num 함수에서 숫자 두 개를 받아 채널에 보내고 리턴한다
> 채널에 숫자 두 개가 저장되어 있는데 close로 채널을 닫아 range 키워드의 반복이 끝나도록 한다
> sum 함수에서 range 키워드로 채널에서 값을 두 개 꺼내 모두 더하고 더한 값을 리턴용 태널에 보낸다
> num 함수가 리턴한 채널에 1과 2가 들어있고 sum 함수에 넣으면 값이 모두 더해진다. 이 값을 리턴한 채널에서 꺼낸다
5. 셀렉트 사용하기
- select 분기문 형식 : select { case <- 채널: 코드 }
select {
case <- 채널1:
// 채널1에 값이 들어왔을 때 실행할 코드
case <- 채널2:
// 채널2에 값이 들어왔을 때 실행할 코드
case <- 채널3:
// 채널3에 값이 들어왔을 때 실행할 코드
defalut:
// 모든 case의 채널에 값이 들어오지 않았을 때 실행할 코드
}
- switch와 비슷하지만 select 키워드 뒤에 검사할 변수를 따로 지정하지 않고 각 채널에 값이 들어오면 실행한다
- defalut로 case에 지정된 채널에 값이 들어오지 않을 때의 코드를 실행할 수 있지만, 적절하게 처리하지 못하면 CPU 코어를 모두 점유하므로 주의해야 한다
- 채널 2개를 생성해 100밀리초, 500밀리초 간격으로 숫자와 문자열을 보내고 꺼내 출력한다
> select 분기문을 이용해 번갈아 가면서 10과 "Hello, world"를 출력한다
> 채널 c2에 "Hello, world"를 보내 500밀리초 대기하고, 채널 c1에는 10을 보내 100밀리초 대기하므로 10이 더 많이 출력됨
> case i := <-c1은 case <-c1처럼 생략해도 괜찮음
> time.Ater 함수를 사용해 시간 제한 처리를 할 수 있다
> 50밀리초 후 현재 시간이 담긴 채널이 리턴된다
- case에서 time.After같은 받기 전용 채널을 리턴하는 함수를 사용할 수 있다
- select 분기문은 채널에 값을 보낼 수 있다 (case 채널 <- 값: 코드)
> select 분기문에서 채널에 값을 보내는 case가 있다면 항상 값을 보낸다
> 채널에 값이 들어왔을 때는 값을 받는 case가 실행된다
> 매번 채널 c1에 값을 보내지만 채널 c2에 값이 들어오면 c2에서 값을 꺼내 출력한다
- 채널 한 개로 select에서 값을 보내거나 받을 수 있음
> 매번 채널에 값을 보내지만 select 분기문이 아닌 다른 쪽에서 채널에 값을 보내 들어오면 값을 받는 case가 실행된다
'Study > Go 언어' 카테고리의 다른 글
[Go언어] 암호화, 프로토콜 (0) | 2020.08.17 |
---|---|
[Go언어] 동기화 객체, 파일처리, JSON (0) | 2020.08.08 |
[Go언어] 반복/조건문, 배열 (0) | 2020.07.25 |
[Go언어] 기본 문법 (자료형, 연산자 등) (0) | 2020.07.18 |
댓글