[Go] 함수 Functions, defer

김무연·2025년 1월 31일

함수

함수는 여러 문장을 묶어서 실행하는 코드 블럭의 단위이다. 타 언어와의 다른 특징이라면 파라미터에 항상 타입을 적어서 정의해주어야 한다. 또한 return 타입 또한 기입해야 하는데 파라미터 괄호 뒤에 기입하도록 되어있다.

이는 C와 같은 언어에서 리턴 타입을 함수명 앞에 쓰는 것과 대조적이다. 또한 함수와 메소드가 여러 값을 반환할 수 있다. 이러한 형태는 마찬가지로 C 프로그램에서 대역 내(in-band) 에러에서 EOF를 나타내기 위해 -1 과 같은 값을 반환하고 주소로 전달한 매개변수를 변환시키는 것과 같은 여러 복잡한 문법을 개선하는데 사용할 수 있다.

package funcexample

func Funcexample()  (int, string) {
	return 2, "string"
}

// 대부분의 패키지의 에러 핸들링
	f, err := os.Open("C:/test.txt")
	if test := 1 ;err != nil {
		fmt.Println("파일 열기 오류:", err)
		fmt.Println(test)

		test := 2
		fmt.Println(test)

		return
	}
	defer f.Close()

위처럼 다중 반환 값으로 인한 os패키지의 Open 메서드처럼, error까지 함께 반환할 수 있다. 이와 같은 형태는 지극히 일반적이며, 유사하게 반환 값으로 참조 매개변수 흉내를 냄으로써 포인터를 전달할 필요가 없게도 만들 수 있다.

package funcexample

import "fmt"

func modifyNum(arg []int) {
	for i := range arg {
		arg[i] *= 2
	}
}

func Funcexample() {
	test := []int{1,2,3,4,5}
	fmt.Println(test, "첫번째")
	
	modifyNum(test)

	fmt.Println(test, "두번째")
}

//
[1 2 3 4 5] 첫번째
[2 4 6 8 10] 두번째

위 함수를 보면 포인터를 전달해 참조된 값을 직접 바꾼 것이 아님에도 불구하고, 값이 변경된 것을 알 수 있다. 슬라이스, 맵, 채널과 같은 타입들은 내부적으로 메모리를 참조하므로, 값을 복사하지 않고도 함수에서 변경한 내용을 호출한 쪽에서 사용할 수 있다.

만약 void처럼 return 값이 없을 경우는 return type을 생략할 수 있다.

이름 있는 결과 인자값 (Named result parameters)

Go함수에서는 반환 "인자" 나 결과 "인자"에 이름을 부여하고, 인자로 들어온 매개변수처럼 일반 변수로 사용할 수 있다. 이름을 부여하면, 해당 변수는 함수가 시작될 때 해당 타입의 제로 값으로 초기화 된다. 함수가 인자 없이 반환문을 수행할 경우에는 결과 매개변수의 현재 값이 반환 값으로 사용된다.

이름을 부여하는것이 필수는 아니지만 이름을 부여하면 코드를 더 짧고 명확하게 만들어 주며, 문서화가 된다. 리턴되는 값들이 여러 개일때 가독성이 증가될 것이다.

func NamedTest() (value, nextPos int) {
	return value, nextPos
}

value, nextPos := funcexample.NamedTest()
fmt.Println(value, nextPos)
    
// RESULT
0 0

익명함수

Go에서는 함수명을 갖지 않는 익명함수가 존재한다. 일반적으로 그 함수 전체를 변수에 할당하거나 다른 함수의 파라미터에 직접 정의되어 사용되곤 한다. 익명함수가 변수에 할당된 이후에는 변수명이 함수명과 같이 취급되며 "변수명(파라미터들)" 형식으로 함수를 호출할 수 있다.

package functiontest

import "fmt"

func Functiontest() {
	sum := func(n ...int) int {
		result := 0
		for _, arg := range n {
			result += arg
		}
		return result
	}

	test := []int{1, 5, 2, 5, 6, 7, 4, 32, 2, 5}

	fmt.Println(sum(test...))

}

일급함수

Go에서 함수는 일급함수로서 Go의 기본 타입과 동일하게 취급되며, 따라서 다른 함수의 파라미터로 전달하거나 다른 함수의 리턴값으로도 사용될 수 있다. 즉, 함수의 입력파라미터나 리턴 파라미터로서 함수 자체가 사용될 수 있다.

함수를 다른 함수의 파라미터로 전달하기 위해서는 익명함수를 변수에 할당한 후 전달하는 방법과 직접 다른 함수 호출 파라미터에 함수를 적는 방법이 있다.

func Functiontest2() {
	add := func(i int, j int) int {
		return i + j
	}

	// 익명함수 변수에 담아 전달
	r1 := calc(add, 10, 20)
	fmt.Println(r1)

	// 직접 파라미터에 함수 정의
	r2 := calc(func(x int, y int) int { return x - y}, 50, 30)
	fmt.Println(r2)
}

func calc(f func(int, int) int, a int, b int) int {
	result := f(a,b)

	return result
}

type문을 사용한 함수의 원형 정의

type 문은 구조체(struct), 인터페이스(interface) 등 custom type등을 정의하기 위해 사용된다. type문은 또한 함수의 원형을 정의하는데 사용될 수 있다.

위의 예제에서 func(x int, y int) int 함수의 원형이 코드 상에 계속 반복됨을 볼 수 있는데, 이 경우 type 문을 정의함으로써 해당 함수의 원형을 간단히 표현할 수 있다.

// 원본
func calc(f func(int, int) int, a int, b int) int {
	result := f(a,b)

	return result
}

// 원형 정의
type calculator func(int, int) int

// calculator 원형 사용
func calcemit(f calculator, a int, b int) int {
	result := f(a,b)

	return result
}

이렇게 함수의 원형을 정의하고 함수를 타 메서드에 전달하고 리턴받는 기능을 타 언어에서 흔히 델리게이트(Delegate)라 부른다.

defer

Go의 defer 문은 defer 를 실행하는 함수가 반환되기 전에 즉각 함수 호출(연기된 함수)를 실행하도록 예약한다. 즉 함수가 return 하기 직전에 지연된 함수(deferred 함수)를 실행하도록 스케쥴링이 가능하게 한다. 함수가 어떤 경로로 return을 하기 전에 release되야 하는 (리소스 해제 및 정리 작업같은) 것을 처리할 때 효과 적인 방법이다.

데이터 베이스 연결, 파일 핸들, 네트워크 연결 등과 같은 리소스를 사용이 끝난 후 반드시 해제해야 하는데, 이러한 작업을 defer를 사용해 예약 해놓거나한다.

package main

import (
	"fmt"
	"os"
)

func main() {
	// 파일 열기
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("파일 열기 오류:", err)
		return
	}
	// 파일을 닫는 작업을 예약
	defer file.Close()

	// 파일 작업 수행

	// 함수가 종료되기 전에 file.Close()가 호출됨
	fmt.Println("파일 작업 완료")
}

defer 함수의 매개 변수들(함수가 메서드일 경우 리시버도 포함되는)은 함수의 호출이 실행될 대가 아닌 defer가 실행될 때 평가된다. 또한 함수가 실행될 때 변수 값이 변하는 것에 대해 걱정할 필요가 없는데, 이는 하나의 defer 호출 위치에서 여러개의 함수 호출을 지연할 수 있음을 의미한다.

package defertest

import "fmt"

func Defertest2() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("defer : %d ", i)
	}
}

//
4,3,2,1,0

위와 같은 예제에서 지연을 안 시킨다면, 0,1,2,3,4 로 출력이 되겠지만, 지연된 함수는 LIFO(후입선출)로 실행되므로, 0출력 > 1출력 > 2출력 > 3출력 > 4출력 으로 스택이 쌓인 후 4출력 > 3출력 > 2출력 > 1출력 > 0출력 순으로 실행이 되어 4,3,2,1,0 으로 실행이 될 것이다.

package defertest

import "fmt"

func trace(s string) string {
	fmt.Println("entering:", s)
	return s
}

func un(s string) {
	fmt.Println("leaving:", s)
}

func a() {
	defer un(trace("a"))
	fmt.Println("in a")
}

func b() {
	defer un(trace("b"))
	fmt.Println("in b")
	a()
}

func Defertest3() {
	b()
}

위 함수는 아래와 같은 결과물을 출력할 것이다

entering: b
in b
entering: a
in a
leaving: a
leaving: b
profile
Notion에 정리된 공부한 글을 옮겨오는 중입니다... (진행중)

0개의 댓글