본문 바로가기
Study/Go 언어

[Go언어] 함수, 구조체, 채널

by Jamie Lim 2020. 8. 1.
사용 교재 : 가장 빨리 만나는 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함수를 사용하면 프로그램이 종료되지 않고 계속 실행된다

런타임 에러 발생

 

런타임 에러 발생 후  recover 로 복구

   

[UNIT 28] 포인터 사용하기

 - Go언어 역시 메모리 주소를 표현하는 포인터를 지원한다

 - 형식 : var 변수명 *자료형

 - new 함수로 지정한 자료형의 크기에 맞는 메모리 공간 할당

 - 메모리를 관리하는 가비지 컬렉션을 지원하기 때문에 메모리 할당 후 해제하지 않아도 된다

    

 

 - 포인터 형 변수에 값을 대입하거나 가져오려면 역참조를 사용한다

 - 변수를 선언할 때 *를 붙이면 포인터형 변수지만, 변수를 사용할 때 *를 붙이면 역참조가 된다

 - 포인터 변수에는 메모리 주소가 저장된다

  

 

 - 일반 변수에 참조(레퍼런스, &)를 사용하면 포인터형 변수에 대입할 수 있다

 - 변수 앞에 &를 붙이면 해당 변수의 메모리 주소를 뜻한다

 

 

 - 메모리 주소를 직접 대입하거나 포인터 연산을 허용하지 않는다

 - 다음의 예시는 불가능

  

 1. 함수에 포인터형 매개변수 사용하기

   - 함수에 정수형 값을 넘기고 함수 안에서 매개변수에 다른 값 대입하더라도 함수 바깥 변수에는 영향을 주지 X

 

 

   - 포인터형 매개변수를 사용하면 값을 바꿨을 때 함수 바깥 변수에 영향을 준다 (메모리 주소를 넘겼기 때문)

 

[UNIT 29] 구조체 사용하기

 - 형식 : type 구조체명 struct { }

 - 구조체 인스턴스는 일반 변수를 선언하는 방법과 같다 (var 변수명 구조체_타입)

 - 구조체 필드의 자료형은 기본 값으로 초기화 된다 (string“”, uint0, float320.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)

 

 

 - 서로 다른 자료형 두 개를 한 개의 인터페이스에 담기

Printer 인터페이스

 

 

 

 - 인터페이스를 선언하면서 초기화하려면 :=를 사용하면 된다

 - 인터페이스에는 ( )(괄호)를 사용해 변수나 인스턴스를 넣어준다

     

 

 - 배열 / 슬라이스 형태로도 인터페이스 초기화 가능

     

 1. 덕 타이핑

   - 덕 타이핑 : 값이나 인스턴스의 실제 타입은 상관하지 않고 구현된 메서드로만 판단하는 방식

Quacker 인터페이스

 

 

 

   - 타입이 특정 인터페이스를 구현하는지 검사하려면 다음과 같이 쓴다

   - 형식 : interface{ }(인터페이스).(인터페이스)

     > Duck 타입의 인스턴스 donald를 빈 인터페이스에 넣고 Quacker 인터페이스와 같은지 확인
     > 첫 번째 리턴값은 검사했던 인스턴스

     > 두 번째 리턴값은 인스턴스가 해당 인터페이스를 구현하고 있는가 여부

 

 2. 빈 인터페이스 사용하기

   - 인터페이스에 아무 메서드도 저장되어 있지 않으면 모든 타입을 저장할 수 있다

func f1(arg interface{}) {
}

 

   - 빈 인터페이스는 다음처럼 Any로 표현할 수 있다

   - 빈 인터페이스 타입은 함수의 매개변수, 리턴값, 구조체의 필드로 사용가능

type Any interface{}

func f2(arg Any) {
}

 

   - 모든 타입을 받아 내용을 출력하는 함수

 

   - 일반 자료형뿐만 아니라 구조체 인스턴스 및 포인터도 빈 인터페이스로 넘길 수 있음

  

   - 인터페이스에 저장된 타입이 특정 타입인지 검사

[UNIT 33] 고루틴 사용하기

 

 

 

 

터미널 창이 종료되지 않음

 

 

 

1 ~ 100 사이의 값 출력

 1. 멀티코어 활용하기

   - Go 언어는 CPU 코어를 한 개만 사용하도록 설정되어 있음

   - 다음은 시스템의 모든 CPU 코어를 사용하는 방법

     * runtime.NumCPU : 현재 시스템의 CPU 코어 개수 구하기

 

 

 

 

 

 

Hello, world! 100이 100번 출력

 

 

[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 := <-c1case <-c1처럼 생략해도 괜찮음

 

     > time.Ater 함수를 사용해 시간 제한 처리를 할 수 있다
     > 50밀리초 후 현재 시간이 담긴 채널이 리턴된다

 

   - case에서 time.After같은 받기 전용 채널을 리턴하는 함수를 사용할 수 있다

   - select 분기문은 채널에 값을 보낼 수 있다 (case 채널 <- 값: 코드)

      > select 분기문에서 채널에 값을 보내는 case가 있다면 항상 값을 보낸다

      > 채널에 값이 들어왔을 때는 값을 받는 case가 실행된다

      > 매번 채널 c1에 값을 보내지만 채널 c2에 값이 들어오면 c2에서 값을 꺼내 출력한다

 

   - 채널 한 개로 select에서 값을 보내거나 받을 수 있음

     > 매번 채널에 값을 보내지만 select 분기문이 아닌 다른 쪽에서 채널에 값을 보내 들어오면 값을 받는 case가 실행된다

 

댓글