>  기사  >  백엔드 개발  >  Golang 지연: 힙 할당, 스택 할당, 오픈 코드 지연

Golang 지연: 힙 할당, 스택 할당, 오픈 코드 지연

WBOY
WBOY원래의
2024-08-07 18:10:56268검색

게시물 일부 발췌입니다. 전체 게시물은 여기서 볼 수 있습니다: Golang Defer: From Basic To Trap.

defer 문은 아마도 우리가 Go를 배우기 시작할 때 가장 먼저 흥미롭다고 생각하는 것 중 하나일 것입니다. 그렇죠?

하지만 많은 사람들을 당황하게 만드는 것 외에도 우리가 사용할 때 종종 다루지 않는 흥미로운 측면이 많이 있습니다.

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

힙 할당, 스택 할당, 오픈 코드 연기

예를 들어 defer 문에는 실제로 3가지 유형이 있습니다(Go 1.22부터, 나중에 변경될 수 있음): 개방형 지연, 힙 할당 지연 및 스택 할당. 각각은 성능이 다르고 가장 잘 사용되는 시나리오도 다르므로 성능을 최적화하려는 경우 알아두면 좋습니다.

이번 토론에서는 기본부터 고급 사용법까지 모든 내용을 다루며, 내부 세부 사항도 아주 조금만 파헤쳐 보겠습니다.

연기 란 무엇입니까?

더 깊이 들어가기 전에 defer에 대해 간단히 살펴보겠습니다.

Go에서 defer는 주변 함수가 종료될 때까지 함수 실행을 지연시키는 데 사용되는 키워드입니다.

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

// Output:
// world
// hello

이 코드 조각에서 defer 문은 fmt.Println("hello")가 기본 함수의 맨 끝에서 실행되도록 예약합니다. 따라서 fmt.Println("world")가 즉시 호출되고 "world"가 먼저 인쇄됩니다. 이후에는 defer를 사용했기 때문에 메인이 끝나기 전 마지막 단계로 "hello"가 출력됩니다.

기능이 종료되기 직전에 나중에 실행할 작업을 설정하는 것과 같습니다. 이는 데이터베이스 연결 닫기, 뮤텍스 해제 또는 파일 닫기와 같은 정리 작업에 매우 유용합니다.

func doSomething() error {
  f, err := os.Open("phuong-secrets.txt")
  if err != nil {
    return err
  }
  defer f.Close()

  // ...
}

위의 코드는 defer의 작동 방식을 보여주는 좋은 예이지만 defer를 사용하는 나쁜 방법이기도 합니다. 이에 대해서는 다음 섹션에서 다루겠습니다.

"좋습니다. 그런데 마지막에 f.Close()를 넣으면 어떨까요?"

여기에는 몇 가지 타당한 이유가 있습니다.

  • 닫기 동작을 열린 부분 근처에 배치하여 논리를 더 쉽게 따르고 파일을 닫는 것을 잊지 않도록 합니다. 파일이 닫혔는지 여부를 확인하기 위해 함수를 아래로 스크롤하고 싶지 않습니다. 그것은 주요 논리에서 주의를 산만하게 합니다.
  • 패닉(런타임 오류)이 발생하더라도 함수가 반환될 때 지연된 함수가 호출됩니다.

패닉이 발생하면 스택이 풀리고 지연된 함수가 특정 순서에 따라 실행됩니다. 이에 대해서는 다음 섹션에서 다루겠습니다.

지연이 누적됩니다.

함수에서 여러 개의 defer 문을 사용하면 '스택' 순서로 실행됩니다. 즉, 마지막으로 지연된 함수가 먼저 실행됩니다.

func main() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  defer fmt.Println(3)
}

// Output:
// 3
// 2
// 1

defer 문을 호출할 때마다 다음과 같이 현재 고루틴의 연결 목록 맨 위에 해당 함수를 추가하게 됩니다.

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

고루틴 지연 체인

그리고 함수가 반환되면 연결리스트를 거쳐 위 이미지에 표시된 순서대로 하나씩 실행됩니다.

하지만 기억하세요. 고루틴의 연결 목록에서 모든 연기를 실행하는 것이 아니라 반환된 함수에서만 연기를 실행한다는 점을 기억하세요. 연기 연결 목록에는 다양한 함수의 많은 연기가 포함될 수 있기 때문입니다.

func B() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  A()
}

func A() {
  defer fmt.Println(3)
  defer fmt.Println(4)
}

따라서 현재 함수(또는 현재 스택 프레임)에서 지연된 함수만 실행됩니다.

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

고루틴 지연 체인

그러나 현재 고루틴의 모든 지연된 함수가 추적되고 실행되는 일반적인 경우가 하나 있는데, 이때 패닉이 발생합니다.

연기, 패닉 및 복구

컴파일 시간 오류 외에도 0으로 나누기(정수만), 범위를 벗어남, nil 포인터 역참조 등 다양한 런타임 오류가 있습니다. 이러한 오류로 인해 애플리케이션이 패닉 상태에 빠지게 됩니다.

패닉은 현재 고루틴의 실행을 중지하고, 스택을 풀고, 현재 고루틴에서 지연된 함수를 실행하여 애플리케이션이 충돌하도록 하는 방법입니다.

예기치 않은 오류를 처리하고 애플리케이션 충돌을 방지하려면 지연된 함수 내에서 복구 기능을 사용하여 패닉 상태의 고루틴에 대한 제어권을 다시 얻을 수 있습니다.

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered:", r)
    }
  }()

  panic("This is a panic")
}

// Output:
// Recovered: This is a panic

일반적으로 사람들은 패닉에 오류를 넣고 복구(..)를 사용하여 오류를 포착하지만 문자열, 정수 등 무엇이든 가능합니다.

In the example above, inside the deferred function is the only place you can use recover. Let me explain this a bit more.

There are a couple of mistakes we could list here. I’ve seen at least three snippets like this in real code.

The first one is, using recover directly as a deferred function:

func main() {
  defer recover()

  panic("This is a panic")
}

The code above still panics, and this is by design of the Go runtime.

The recover function is meant to catch a panic, but it has to be called within a deferred function to work properly.

Behind the scenes, our call to recover is actually the runtime.gorecover, and it checks that the recover call is happening in the right context, specifically from the correct deferred function that was active when the panic occurred.

"Does that mean we can’t use recover in a function inside a deferred function, like this?"

func myRecover() {
  if r := recover(); r != nil {
    fmt.Println("Recovered:", r)
  }
}

func main() {
  defer func() {
    myRecover()
    // ...
  }()

  panic("This is a panic")
}

Exactly, the code above won’t work as you might expect. That’s because recover isn’t called directly from a deferred function but from a nested function.

Now, another mistake is trying to catch a panic from a different goroutine:

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered:", r)
    }
  }()

  go panic("This is a panic")

  time.Sleep(1 * time.Second) // Wait for the goroutine to finish
}

Makes sense, right? We already know that defer chains belong to a specific goroutine. It would be tough if one goroutine could intervene in another to handle the panic since each goroutine has its own stack.

Unfortunately, the only way out in this case is crashing the application if we don’t handle the panic in that goroutine.

Defer arguments, including receiver are immediately evaluated

I've run into this problem before, where old data got pushed to the analytics system, and it was tough to figure out why.

Here’s what I mean:

func pushAnalytic(a int) {
  fmt.Println(a)
}

func main() {
  a := 10
  defer pushAnalytic(a)

  a = 20
}

What do you think the output will be? It's 10, not 20.

That's because when you use the defer statement, it grabs the values right then. This is called "capture by value." So, the value of a that gets sent to pushAnalytic is set to 10 when the defer is scheduled, even though a changes later.

There are two ways to fix this.

...

Full post is available here: Golang Defer: From Basic To Trap.

위 내용은 Golang 지연: 힙 할당, 스택 할당, 오픈 코드 지연의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.