[golang] 지연처리 defer

지연 처리 defer

defer은 함수 앞에 쓰이는 키워드로써 특정 문장 혹시 함수를 감싸고 있는 함수 내에서 제일 나중에, 끝나기 직전에 실행하게 하는 용법이다. 자바에서는 try-finally와 비슷한 용법이다. 자바에서는 이와 같은 형식인데

try {
	메모리 할당 및 구문 실행
} catch {
	예외 처리(논리적 오류)
} finally {
	마지막에 꼭 실행 및 할당된 공간 반납
}

go의 defer은 블록이 필요한 것도 아니고, 특정 위치나 형식이 필요한 것도 아니다. 단지 함수 앞에 defer을 명시함으로써 사용한다. 쉽게 말해서, 프로그램의 흐름에 분기가 많아 '예외처리'가 많아 복잡할때 유용하게 사용된다. defer을 사용하면 흐름중간에 에러(예외)가 발생해도 마지막에 꼭 실행하고 프로그램을 종료하지 않는다.

package main

import "fmt"

func main(){
	defer fmt.Println("world")
	fmt.Println("hello ")
}

위 코드만 보면 world hello가 나올 것 같지만, hello world가 출력된다. 그 이유는 지연실행 defer키워드를 world 코드 앞에 입력했기 때문에 hello먼저 실행하고, main함수가 종료되기 직전에 world가 실행된다. 이를 활용해 defer라는 프로그램을 실행하면서 예상하지 못한 에러(panie)가 발생했을 때 프로그램을 종료하지 않고 defer을 사용할 수 있다.

package main

import "fmt"

func main(){
	var a,b int = 10,0
	defer fmt.Println("done")
	
	result := a / b
	fmt.Println(result)
}

위 코드에서는 어떤 수를 0으로 나누면서 에러가 발생한다. 이는 패닉 에러의 대표적인 예이다. 코드 문법상으로는 아무 문제도 없는데 프로그램이 시작하고 연산을 하면 에러가 발생한다. 이때 에러가 발생하고 바로 종료되는 것이 아니라 미리 선언해두었던 defer구문이 마지막으로 실행되고 종료되는 것이다. 그리고 만약에 defer구문을 에러가 발생하는 코드 뒤에 선언하면 호출되지 않고 프로그램이 종료된다. 따라서 에러가 발생하는 코드 전에 선언해야한다.

그렇다면 만약에 defer을 사용한 함수들이 여러개면 어떤 함수가 먼저 호출될까?

package main

import "fmt"

func hello() {
	fmt.Println("Hello")
}

func world() {
	fmt.Println("world")
}

func main() {
	defer world()
	hello()
	
	for i := 0; i <3; i++ {
		defer fmt.Println(i)
	}
    
    
    //결과 
    //hello 
    //1
    //2
    //world
}

위 코드에서는 당연히 defer를 사용한 함수가 더 역순으로 실행된다. 이것은 자료구조의 스택(LIFO)와 동일한데 제일 나중에 지연호출한 함수가 제일 먼저 실행되는 것이다.(지연호출된 함수 중에서)
위의 defer키워드는 파일을 열고 닫을 때 많이 활용된다. 파일을 열거나 읽어들이면서 에러가 발생하면 파일을 닫을 수 없기 때문이다.

package main

import (
	"fmt"
	"os" // 파일 관련 함수를 다루기 위해
)

func Helloworld(){
	file, err := os.Open("test.txt") // 같은 디렉토리 내의 파일을 연다. 이때 Open함수의 반환값은 파일과 에러값이므로 두 개이므로 각각 변수에 초기화한다.  
	defer file.Close()
	
	
	//nil아니라면 즉, 에러값이 존재하면!!!! err값을 출력하고 함수를 종료한다. 
	// 이때 지연처리 용법이 필요하다. Helloworld함수가 종료되기 전에 defer file.close를 실행한다. 즉 파일을 닫는다.
	// 그래서 프로그램이 오류가 발생하더라도 defer키워드를 사용해 파일을 닫고 다음 코드를 실행할 수 있다. 
	if err !=. nil{ 
		fmt.Println(err)
		return 
	}
	
	
	//그리고 만약 에러가 없다면,  byte형 슬라이스를 생성하고, 
	buf := make([]byte,1024) // make(슬라이스 타입,슬라이스 길이)
	
	// 에러 처리와 file.Read로 불러온 파일을 읽고 초기화한다. 
	//그리고 마찬가지로 err값이 있으면 err출력하고 return 전에 defer을 실행한다.
	if _, err = file.Read(buf); err != nil{
		fmt.Println(err)
		return 
	}
	
	fmt.Println(string(buf)) // 그리고 파일 내용 출력
}

func main(){
	Helloworld()
	fmt.Println("Done")
}

위의 코드는 test.txt파일이 없기 때문에 오류가 나지만, 만약에 같은 디렉토리에 파일에 아무것도 넣지않고 만들어 실행했을 때 "EOF"라고 에러가 출력되고 프로그램이 종료되는 것이 아니라 Done이 출력된다. 반대로 파일에 내용을 넣고 실행하면, 에러처리가 되지 않았기 때문에 출력이 된 후에 Done이 출력된다.

종료하는 panic()

defer과 다르게 panic은 겉으로 보이게 아무런 문제가 없는데 실행해보니 에러가 발생해서 프로그램을 종료하는 기능을 한다. 반대로 말해서 문법 자체를 잘못 입력했을 때 발생하는 에러는 panic이 아니다!
이때, 문제가 없는데 오류가 발생하는 코드가 뭐지?하고 생각한다면, 오류와 예외의 차이를 봐야한다.

오류는 프로그램상 허용하지 않는 문법과 같은 비정상적인 상황에 발생하는 것을 말한다.
ex) int형 변수에 30.5를 초기화하면 오류가 발생한다.
예외는 프로그램이 실행되면서 논리상으로 부적합한 상황이 발생하는 것을 말한다.
ex)나눗셈을 할때 10/0은 논리적 예외 상황이다. 이러한 것들을 프로그램이 오류라고 생각하지 않기 떼문에 코드 상에서 따로 예외 처리를 해야한다.

package main

import "fmt"

func panicTest() {
	var a = [4]int{1,2,3,4}
	
	defer fmt.Println("Panic done")
	
	for i := 0; i < 10; i++ {
		fmt.Println(a[i]) // 선언한 배열의 개수보다 큰 인덱스 값에 접근해 Panic발생함 하지만 defer구문이 먼저 선언되어서 종료 전에 defer이 먼저 출력되고 종료. 따라서 main의 helloworld는 실행되지 않음.(panic이 발생해 프로그램이 종료돼서 )
	}		
}

func main() {
	panicTest()

	fmt.Println("Hello, world!")
}

또한 panic은 에러뿐만 아니라 사용자가 panic()함수를 이용해 예외 상활일때 직접 Panic에러를 발생시킬 수 있다. 여기서 Panic() 함수 안에 에러메세지를 사용자 설정으로 출력할 수 있다.

package main

import "fmt"

func main() {
    var opt int
    var num1, num2, result float32

    fmt.Print("1.덧셈 2.뺄셈 3.곱셈 4.나눗셈 선택:")
    fmt.Scan(&opt)
	if opt != 1 && opt != 2 && opt != 3 && opt != 4 {
		panic("1, 2, 3, 4중에 하나만 입력해야합니다!") // 사용자 설정 panic
	}
    fmt.Print("두 개의 실수 입력:")
    fmt.Scan(&num1, &num2)

    if opt == 1 {
        result = num1 + num2
    } else if opt == 2 {
        result = num1 - num2
    } else if opt == 3 {
        result = num1 * num2
    } else if opt == 4 {
        result = num1 / num2
    }
	
    fmt.Printf("결과: %f\n", result)
}

위 코드는 벗어나는 숫자를 입력했을 때 panic()함수를 호출해 에러를 발생시켜 프로그램을 종료한다 하지만 예시는 예시일뿐 에러가 났다고 프로그램을 종료하는 코드는,,짜지말자,,

panic을 막는 recover() 함수

에러가 생기는 상황에 바로 panic을 발생시키고 프로그램을 종료하는 것은 안 좋은 방법이다. 그렇다면 에러가 생기는 상황만 예외 처리를 하는 방법은 어떨까? 자바에서는 try-catch 구문을 사용해 예외 처리를 하는데 recove() 함수도 이와 같은 역할을 한다. panic 상황이 생겼을 때 프로그램을 종료하지 않고 예외처리를 한다. panic()함수와 recover() 함수를 사용해 프로그램 흐름을 사용자가 미리 예외처리를 할 수 있는데 위에서 언급한 것처럼 panic상황에서 프로그램이 종료되지 않고 어떤 구문을 실행시키려면 defer구문을 사용해야 한다. 따라서 예외 처리를 하기 위해서는 recover()함수와 defer구문을 항상 같이 사용해야한다.

package main

import "fmt"

func panicTest() {
	defer func() {
		r := recover() //복구 및 에러 메시지 초기화
		fmt.Println(r) //에러 메시지 출력 
	}()
	
    var a = [4]int{1,2,3,4}
    
    for i := 0; i < 10; i++ { //panic 발생
        fmt.Println(a[i])
    }       
}

func main() {
    panicTest()

    fmt.Println("Hello, world!") // panic이 발생했지만 계속 실행됨
}

위의 코드를 보면 panic이 발생하기 전에 지연처리한 defer익명 함수가 호출되고 익명 함수 내의 revover()함수가 호출되면서 panic을 복구한다. 따라서 프로그램이 종료되지 않고, main의 hello world가 호출되어 프로그램이 종료되지 않았다는 것을 증명한다. 즉 panic이 발생한 해당 함수는 종료되고 다음 코드를 실행한다. 정리하면

recover()함수는

  • panic이 발생해 프로그램이 종료되는 것을 막고 복구한다.
  • 프로그램이 종료되기 전에 실행되어야 함으로 defer가 선언된 함수 안에서 쓰인다.
  • 에러 메세지를 반환한다. 따라서 변수에 초기화해서 에러 메세지를 출력할 수 있다.
package main

import "fmt"

func main() {
	defer func() {
    
        //조건식 앞에 변수를 선언하고 식 입력
	// 조건식 전에 정의된 변수는 해당 조건문 블록에서만 사용할 수 있다. (맨 밑에 예시 존재)
		if r := recover(); r != nil{
			fmt.Println("!!!!!!!!!!!!!!!!!!!")
			// 만약 r이 있으면 (에러가 생기면)
			fmt.Println(r) // 에러 출력하고 main가서 result 출력 		
			
			main()
		}		
	}()
	
	var num1, num2 int
	fmt.Scanln(&num1, &num2)
	
	result := num1 / num2 //여기서 만약 panic이 나면 -> defer 실행 -> defer의 recover함수 실행

	fmt.Println(result)
}



  • 조건식에 간단한 문제 실행하기 예시!
package main

import "fmt"

func main(){
	var num int
	
	fmt.Println("정수입력 : ")
	fmt.Scanln(&num)
	
	
	//조건식 앞에 변수를 선언하고 식 입력
	// 조건식 전에 정의된 변수는 해당 조건문 블록에서만 사용할 수 있다. 
	if val := num *2; val == 2{
		fmt.Print("hello\n")
	} else if val := num * 3; val == 6 {
		fmt.Print("world\n")
	} else {
		fmt.Print("worng number..\n")
	}
}

좋은 웹페이지 즐겨찾기