본문으로 바로가기

defer

golang.org/ref/spec#Defer_statements

함수가 끝났을 때 작동할 동작을 정의할 수 있습니다. 보통 메모리 릭을 막기 위해 close, flush를 통해 닫아주는 역할을 합니다.

대부분 OS의 내부 자원을 사용하는 경우에 내부 자원을 돌려주는 코드가 필요합니다. 예를 들어 파일관련 동작을 위해서 파일 핸들을 OS에 요청한 후 사용했다면 끝난 후에는 닫아줄 필요가 있겠죠. 

 

이럴 때 defer가 유용합니다. 우선 가장 간단히 사용하는 방법은 아래와 같습니다.

func lenAndUpper(name string) (length int, uppercase string) {
	defer fmt.Println("I'm done")
    
	length = len(name)
	uppercase = strings.ToUpper(name)
	return length, uppercase
}

func main() {
	totalLen, upperString := lenAndUpper("Martin") // 할당이 끝나고 defer 동작
	fmt.Println(totalLen, upperString)
}

// output
I'm done 
6 MARTIN

 

파일 생성을 한 후 defer를 이용하여 Close를 해보겠습니다.

여기서 살펴볼 수 있는 또 하나의 특징은, defer는 선언된 순서의 역순으로 호출된다는 것입니다.

func main() {
	f, err := os.Create("newFile.txt")
	if err != nil {
		fmt.Println(err)
		fmt.Println("fail to create a file")
		return
	}
	
	// F가 붙으면 파일 관련 입출력
	fmt.Fprintln(f, "hello")

	// 맨 아래에 정의할 필요는 없지만 가독성을 위해 아래에 배치함
	// defer는 선언된 순서의 역순으로 호출됨.
	defer fmt.Println("defer end")
	defer f.Close()
	defer fmt.Println("defer start")
}

 

 

함수 리터럴(익명함수)

익명함수입니다. Go에서는 이걸 함수 리터럴이라고 부르구요. 

 

1) 즉시 실행 함수로 사용

 

함수들을 만드는 것에 단점이 있는데 바로 '프로그램 속도 저하'입니다. 왜냐하면

  • 함수 선언 자체가 프로그래밍 전역으로 초기화되면서 메모리를 잡아먹기 때문입니다.
  • 기능을 수행할 때마다 함수를 찾아서 호출해야하기 때문입니다.

사실 확인하고보면 이름 없이 함수를 만들고 즉시 실행시키는 방식이다. js에도 이런 방식으로 즉시 실행시켜와서 그렇게 생소한 방식은 아니다. IFFE 즉시 실행 함수와 비슷한 것으로 이해하면 될 듯

package main

import "fmt"

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

	func(a int, b int) {
		result := a + b
		fmt.Println(result)
	}(1, 3)
}

 

2) 함수 리터럴 내부에서 외부 변수 사용(Capture)시 값복사가 아니라 참조 복사가 됨!

 

Go는 기본적으로 call by value다. 따라서 아래 addTen 함수는 인자를 받으면 스코프 내 지역변수를 선언하여 함수 내용을 처리하고

함수 스코프를 벗어나면 없어지게 된다. 따라서 아래 코드의 결과물은 10이 아니라 0이다.

func addTen(a int) {
	a += 10
}

func main() {
	i := 0
	addTen(i)
	fmt.Println(i) // 0
}

 

그런데 아래 코드를 살펴보자. 아래와 같은 값으로서의 함수를 사용하게 되면 외부 변수를 내부로 가져와서 조작합니다.

이 경우 값복사가 아니라 참조 형태로 가지고 오게 되어 원본의 값을 변형할 수 있게 되는 것이죠.

func main() {
	i := 0

	innerAddTen := func() {
		i += 10
	}
	innerAddTen()

	fmt.Println(i) // 10
}

 

참고로 아래는 함수 리터럴이 아니라 함수를 값으로서 사용한 겁니다. 일반 함수 정의랑 똑같아요

함수의 인자로 넣어버리면 이 녀석은 또 값 복사를 통해서 연산을 처리하므로 0을 처리합니다.

func main() {
	i := 0

	innerAddTen := func(a int) {
		a += 10
	}
	innerAddTen(i)

	fmt.Println(i) // 0
}

 

결론적으로

1) 함수 리터럴을 활용한 내부 함수는 외부 변수를 참조값으로 가져오며

2) call by value와 call by reference를 잘 이해하시고 사용해야 한다는 겁니다.

 

 

 

값으로서의 함수 => 내부 함수 => 커링 가능

 

함수 리터럴과 일반 함수를 값으로 사용하는 것은 매우 다른 것입니다! 후술합니다.

 

JS에서 많이들 해보셨죠. js를 가져와 보았습니다.

// 함수 선언식. 호이스팅에 영향을 받음(js 특성)
function foo() {...}

// 함수 표현식. 호이스팅에 영향을 받지 않음(js 특성)
const fn = function foo() {...}

 

go도 똑같습니다. 아래와 같이 함수 표현식을 사용할 수 있습니다. 이렇게 사용되는 것에서 짐작하셨겠지만 Go에서 함수는 1급 시민입니다. 그러니까 함수를 값처럼 다룬다는 겁니다.

 

* 1급 시민 : 변수에 담을 수 있고, 함수의 인자로 전달할 수 있고, return 할 수 있다. (대개 숫자, 문자형은 1급 시민)

darrengwon.tistory.com/595

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

	fmt.Println(add(3, 5))
}	

 

함수를 반환할 수 있는 함수에는 타입을 어떻게 지정해야 할까요? 조금 번거롭지만 아래와 같이 해줄 수 있습니다.

다른 이야기지만, 함수를 반환하는 함수라는 점에서 커링을 사용할수도 있겠죠.

// int형 인자 2개를 받고 int를 반환하는 함수를 return함
func executeFunc() func (int, int) int {
	return func(a, b int) int {
		return a + b
	}
}

func main() {
	fmt.Println(executeFunc()(3, 5)) // 8
}	

 

typescript할 때도 그랬지만, 이렇게 반환하는 함수의 타입을 직접 적어주는 건 실수할 가능성도 높아서 함수 시그니쳐를 별도로 적어주는 것이 좋습니다. 아래처럼요

type opFunc func(int int) int

// int형 인자 2개를 받고 int를 반환하는 함수를 return함
func executeFunc() opFunc {
	return func(a, b int) int {
		return a + b
	}
}

 

 

함수 리터럴과 함수 타입 변수는 다릅니다!

넵 다릅니다.

func main() {
	i := 0

	// 함수 타입 변수. 함수를 변수에 넣었을 뿐임. 
	// 값복사를 하고, 함수 스코프 벗어나면 가비지 컬렉팅됨
	innerAddTenFunc := func(a int) {
		a += 10
	}

	// 함수 리터럴. 참조로 변수 가져와 원본을 변화시킴.
	innerAddTenLiteral := func() {
		i += 10
	}

	innerAddTenFunc(i)
	fmt.Println(i) // 0. 원본 유지.
	
	innerAddTenLiteral()
	fmt.Println(i) // 10. 변화됨
}

 

아래는 간단히 덧셈 함수를 한 번 함수로 덮어 씌운 코드입니다.

이 경우 op는 함수 타입 변수입니다. 외부 변수 참조 없이 값 복사를 한 결과물을 출력합니다.

func ExecuteFunc(a, b int, op func(int, int) int ) {
	fmt.Println(op(a, b))
}

func main() {
	op := func(a, b int) int {
		return a + b
	}
	ExecuteFunc(3, 5, op)
}

 

 

 

callback

 

js에서 매번 했던 것이므로 이론은 생략하고 코드로 보겠습니다. js만큼 간단하게 생기지는 않았습니다.

Go에서 callback을 구현하는 방법은 아래와 같이 함수의 인자로 함수를 받아서 구현해야 합니다.

func main() {
	nums := []int{1, 2, 3, 4, 5}
	s := even(sum, nums...)
	fmt.Println(s)
}

func sum(x ...int) int {
	total := 0
	for _, v := range x {
		total += v
	}
	return total
}

func even(f func(x ...int) int, s ...int) int {
	var ss []int
	for _, v := range s {
		if v%2 == 0 {
			ss = append(ss, v)
		}
	}

	evenSum := f(ss...)
	return evenSum
}

 

 

closure

 

js에서 사용했으므로 이론은 생략하겠습니다. 간략하게 설명하자면 내부 함수가 외부 함수의 스코프에 있는 변수에 접근할 수 있다는 겁니다. 클로저는 darrengwon.tistory.com/633에도 정리해뒀습니다.

 

x가 외부 스코프에 있음에도 내부함수가 사용하는 꼴을 보실 수 있습니다.

func main() {
	num := inc()()
	fmt.Println(num)
}

func inc() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체