Go에서 흐름 제어: break, continue, 그리고 피해야 할 goto 탐구
Emily Parker
Product Engineer · Leapcell

명확성, 단순성 및 동시성에 중점을 둔 Go는 프로그램 흐름을 제어하는 간단한 메커니즘을 제공합니다. 다른 언어에서 발견되는 더 복잡하고 종종 혼란스러운 생성물 중 일부를 피하지만, 루프를 관리하고 실행을 지시하는 필수 문을 여전히 제공합니다. 이 글은 루프 조작을 위한 기본 도구인 break
와 continue
를 살펴보고, 관용적인 Go에서는 일반적으로 사용이 권장되지 않는 문인 goto
에 대해 신중하게 논의할 것입니다.
루프 탐색: break
및 continue
루프는 코드 블록의 반복 실행을 허용하는 프로그래밍의 초석입니다. Go의 for
루프는 매우 다재다능하며, 다른 언어의 for
, while
, do-while
루프의 목적을 수행합니다. 이러한 루프 내에서 break
와 continue
는 반복에 세밀한 제어를 제공합니다.
break
: 루프 조기 종료
break
문은 가장 안쪽의 for
, switch
또는 select
문을 즉시 종료하는 데 사용됩니다. break
가 발견되면 제어 흐름은 종료된 생성물 바로 뒤의 문으로 점프합니다.
예제 1: for
루프에서의 기본 break
시퀀스에서 100보다 큰 첫 번째 짝수를 찾으려고 한다고 가정해 봅시다.
package main import "fmt" func main() { fmt.Println("---" + " Using break" + " ---") for i := 1; i <= 200; i++ { if i%2 == 0 && i > 100 { fmt.Printf("Found the first even number > 100: %d\n", i) break // Condition is met, exit the loop } } fmt.Println("Loop finished or broken.") }
이 예에서 i
가 102가 되면 if
조건이 참이 되고 "Found..."가 출력되며 break
가 루프를 중지합니다. break
가 없으면 루프는 200까지 계속될 것이며, 이는 첫 번째 일치만 필요한 경우 비효율적입니다.
예제 2: 레이블이 있는 중첩 루프에서의 break
때때로 중첩된 루프가 있고 내부 루프에서 외부 루프를 중단해야 할 수 있습니다. Go는 레이블을 사용하여 이를 허용합니다. 레이블은 콜론(:
)으로 구분된 식별자로, 중단하려는 문 앞에 배치됩니다.
package main import "fmt" func main() { fmt.Println("\n" + "---" + " Using break with labels" + " ---") OuterLoop: // Label for the outer loop for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { fmt.Printf("i: %d, j: %d\n", i, j) if i == 1 && j == 1 { fmt.Println("Breaking out of OuterLoop from inner loop...") break OuterLoop // This breaks the OuterLoop, not just the inner one } } } fmt.Println("After OuterLoop.") }
OuterLoop:
레이블과 break OuterLoop
이 없으면 내부 루프가 중단되지만 외부 루프는 반복을 계속합니다(예: i=2
실행). 레이블은 여러 중첩된 생성물에 걸쳐 흐름을 제어하는 수술적인 방법을 제공합니다.
continue
: 현재 반복 건너뛰기
continue
문은 루프의 현재 반복의 나머지 부분을 건너뛰고 다음 반복으로 진행하는 데 사용됩니다. 루프 전체를 종료하지는 않습니다.
예제 3: for
루프에서의 기본 continue
1에서 10까지의 홀수만 출력해 봅시다.
package main import "fmt" func main() { fmt.Println("\n" + "---" + " Using continue" + " ---") for i := 1; i <= 10; i++ { if i%2 == 0 { continue // Skip even numbers, go to the next iteration } fmt.Printf("Odd number: %d\n", i) } fmt.Println("Loop completed.") }
여기서 i
가 짝수일 때 i%2 == 0
이 참이 되면 continue
는 즉시 i
의 다음 값으로 점프하고( i
를 증가시키고 루프 조건을 다시 평가) 짝수에 대한 fmt.Printf
문을 건너뜁니다.
예제 4: 레이블이 있는 continue
(덜 일반적이지만 가능)
break
와 유사하게 continue
도 레이블과 함께 사용할 수 있지만, 덜 자주 보입니다. 레이블과 함께 사용될 때 continue
는 레이블이 지정된 루프의 현재 반복의 나머지 부분을 건너뛰고 해당 루프의 다음 반복으로 진행합니다.
package main import "fmt" func main() { fmt.Println("\n" + "---" + " Using continue with labels" + " ---") OuterContinueLoop: for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if i == 1 && j == 0 { fmt.Printf("Skipping i: %d, j: %d and continuing OuterContinueLoop...\n", i, j) continue OuterContinueLoop // Skips remaining inner loop iterations for i=1, and immediately moves to the next iteration of OuterContinueLoop (i=2) } fmt.Printf("i: %d, j: %d\n", i, j) } } fmt.Println("After OuterContinueLoop.") }
이 예에서 i
가 1이고 j
가 0일 때 continue OuterContinueLoop
문이 실행됩니다. 이는 현재 i=1
에 대한 내부 루프가 중단되고 프로그램이 OuterContinueLoop
의 다음 반복인 i=2
로 직접 진행됨을 의미합니다.
goto
문: 극도로 주의해서 진행
Go에는 goto
문이 포함되어 있으며, 이를 통해 동일한 함수 내의 레이블이 지정된 문으로 무조건 점프할 수 있습니다. 존재하지만, 현대 프로그래밍 관행, 특히 Go에서는 그 사용이 널리 권장되지 않습니다.
구문:
goto label; // Transfers control to the statement marked by 'label:' // ... label: // statement;
goto
가 권장되지 않는 이유는 무엇입니까?
- 복원력 및 가독성 (스파게티 코드):
goto
는 코드를 읽고 이해하기 어렵게 만듭니다. 제어 흐름이 임의로 점프하여 실행 경로를 추적하고 프로그램 논리를 이해하기 어려운 "스파게티 코드"를 초래할 수 있습니다. - 유지보수성:
goto
를 사용하는 코드는 유지보수, 디버깅 및 리팩터링이 매우 어렵습니다. 코드의 한 부분에서 발생하는 변경은 멀리 떨어진goto
점프 때문에 의도하지 않은 결과를 초래할 수 있습니다. - 구조화된 프로그래밍: 현대 프로그래밍 패러다임은
if-else
,for
,switch
및 함수 호출과 같은 생성물을 통해 제어 흐름이 관리되는 구조화된 프로그래밍을 강조합니다. 이러한 생성물은 더 명확하고 예측 가능하며 관리하기 쉬운 코드로 이어집니다.
Go의 goto
에 대한 특정 제한 사항:
Go는 goto
에 몇 가지 중요한 제한 사항을 적용하여 다른 언어에서 발견되는 몇 가지 일반적인 함정을 방지합니다.
- 블록이 현재 블록과 다른 블록에 정의되어 있거나,
goto
문 뒤에 시작되지만goto
문을 포함하는 블록 내에 있는 레이블로goto
할 수 없습니다. 기본적으로 블록으로 점프하거나 변수 선언을 건너뛸 수 없습니다. - 변수 선언을 건너뛰기 위해
goto
할 수 없습니다. goto
와 해당 레이블은 동일한 함수 내에 있어야 합니다.
예제 5: Go에서의 goto
사용 가능 사례 (드뭄)
Go에서 goto
를 고려할 수 있는 몇 안 되는 시나리오 중 하나는 일련의 작업에서 오류가 발생한 후 리소스를 정리하는 것입니다. 특히 defer
가 적합하지 않거나 긴 if err != nil
검사가 번거로워질 때입니다. 이때도 명명된 반환 값과 defer
가 종종 선호됩니다.
가상 리소스 할당 시나리오를 고려해 봅시다.
package main import ( "fmt" "os" ) func processFiles(filePaths []string) error { var f1, f2 *os.File var err error // Step 1: Open file 1 f1, err = os.Open(filePaths[0]) if err != nil { fmt.Printf("Error opening %s: %v\n", filePaths[0], err) goto cleanup // Jump to cleanup if error } defer f1.Close() // Defer close for f1 if successfully opened // Step 2: Open file 2 f2, err = os.Open(filePaths[1]) if err != nil { fmt.Printf("Error opening %s: %v\n", filePaths[1], err) goto cleanup // Jump to cleanup if error } defer f2.Close() // Defer close for f2 if successfully opened // Step 3: Perform operations with f1 and f2 fmt.Println("Both files opened successfully. Performing operations...") // ... (actual file processing logic) // In a more complex scenario, imagine more steps here // where errors at any point need a centralized cleanup. cleanup: // This is the label for cleanup fmt.Println("Executing cleanup logic...") // The defer statements above handle closing the files that were successfully opened. // Any other specific cleanup not handled by defer could go here. return err // Return the error encountered (or nil if successful) } func main() { err := processFiles([]string{"non_existent_file1.txt", "non_existent_file2.txt"}) if err != nil { fmt.Println("Processing failed:", err) } err = processFiles([]string{"existing_file.txt", "non_existent_file.txt"}) // Assume existing_file.txt exists for this test if err != nil { fmt.Println("Processing failed:", err) } else { fmt.Println("Processing completed successfully.") } }
참고: Go에서 리소스 정리를 처리하는 관용적인 방법은 종종 defer
문을 통해 이루어집니다. 이전 goto
예제는 defer
를 더 효과적으로 사용하거나 함수 흐름을 조기 반환하거나 도우미 함수를 사용하도록 리팩터링할 수 있습니다. goto
버전은 여기서 종종 권장되는 것이 아니라 단순히 볼 수 있는 드문 패턴 중 하나로만 제시됩니다.
goto
예제를 defer
및 조기 반환을 사용하여 리팩터링:
더 관용적인 Go 접근 방식은 다음과 같이 일반적으로 더 명확합니다.
package main import ( "fmt" "os" ) func processFilesIdiomatic(filePaths []string) error { // Open file 1 f1, err := os.Open(filePaths[0]) if err != nil { return fmt.Errorf("error opening %s: %w", filePaths[0], err) } defer f1.Close() // Ensures f1 is closed when function exits // Open file 2 f2, err := os.Open(filePaths[1]) if err != nil { return fmt.Errorf("error opening %s: %w", filePaths[1], err) } defer f2.Close() // Ensures f2 is closed when function exits fmt.Println("Both files opened successfully. Performing operations...") // ... (actual file processing logic) return nil // No error } func main() { fmt.Println("\n" + "---" + " Idiomatic File Processing" + " ---") // For testing, let's create a dummy file dummyFile, _ := os.Create("existing_file.txt") dummyFile.Close() defer os.Remove("existing_file.txt") // Clean up dummy file err := processFilesIdiomatic([]string{"non_existent_file_idiomatic1.txt", "non_existent_file_idiomatic2.txt"}) if err != nil { fmt.Println("Idiomatic processing failed (expected):", err) } err = processFilesIdiomatic([]string{"existing_file.txt", "non_existent_file_idiomatic.txt"}) if err != nil { fmt.Println("Idiomatic processing failed (expected):", err) } else { // This path would only be taken if both files existed fmt.Println("Idiomatic processing completed successfully (unlikely without creating both files).") } }
이 관용적인 버전은 defer
가 성공적으로 획득된 각 리소스에 대한 정리를 자연스럽게 처리하고 조기 반환이 임의 점프 없이 제어 흐름을 단순화하기 때문에 일반적으로 선호됩니다.
결론
Go는 강력하고 명확한 제어 흐름 문 집합을 제공합니다. break
및 continue
는 루프 반복을 효율적으로 관리하는 데 필수적인 도구이며, 레이블과의 사용은 중첩된 구조에서 정밀한 제어를 제공합니다. goto
는 Go에 존재하지만, 읽기 어렵고 유지보수하기 어려운 "스파게티 코드"를 생성할 가능성 때문에 사용이 강력히 권장되지 않습니다. Go의 철학은 단순성과 명시적인 제어를 지향하며, break
, continue
, 잘 구조화된 if
, for
, switch
문은 프로그램 흐름을 관리하는 데 거의 항상 충분하고 우수합니다. 명확하고 순차적이며 구조화된 코드를 목표로 하십시오. 미래의 자신과 동료들이 감사하게 될 것입니다.