4 Go 언어 비판
게시 됨: 2022-03-11Go(Golang이라고도 함)는 사람들이 가장 관심을 갖고 있는 언어 중 하나입니다. 2018년 4월 현재 TIOBE 지수에서 19위를 차지하고 있습니다. 점점 더 많은 사람들이 PHP, Node.js 및 기타 언어에서 Go로 전환하여 프로덕션 환경에서 사용하고 있습니다. 많은 멋진 소프트웨어(예: Kubernetes, Docker 및 Heroku CLI)가 Go를 사용하여 작성되었습니다.
그렇다면 Go의 성공 비결은 무엇일까요? 언어 내부에는 정말 멋지게 만드는 많은 것들이 있습니다. 그러나 Go를 그렇게 유명하게 만든 주요 요인 중 하나는 제작자 중 한 명인 Rob Pike가 지적한 바와 같이 단순함입니다.
단순함이 좋습니다. 많은 키워드를 배울 필요가 없습니다. 그것은 언어 학습을 매우 쉽고 빠르게 만듭니다. 그러나 다른 한편으로 개발자는 다른 언어에 있는 일부 기능이 부족하기 때문에 장기적으로 해결 방법을 코딩하거나 더 많은 코드를 작성해야 합니다. 불행히도 Go는 설계상 많은 기능이 부족하고 때로는 정말 짜증스럽습니다.
Golang은 개발 속도를 높이기 위한 것이지만 많은 상황에서 다른 프로그래밍 언어를 사용하여 작성하는 것보다 더 많은 코드를 작성하고 있습니다. 아래의 Go 언어 비평에서 그러한 경우를 설명하겠습니다.
4가지 Go 언어 비판
1. 함수 오버로딩 및 인수의 기본값 부족
여기에 실제 코드 예제를 게시하겠습니다. Golang의 Selenium 바인딩 작업을 할 때 세 개의 매개변수가 있는 함수를 작성해야 했습니다. 그 중 2개는 선택사항이었습니다. 구현 후의 모습은 다음과 같습니다.
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
함수를 오버로드하거나 기본값을 전달할 수 없었기 때문에 세 가지 다른 함수를 구현해야 했습니다. Go는 의도적으로 제공하지 않습니다. 실수로 전화를 잘못 걸면 어떻게 될지 상상해 보세요. 다음은 예입니다.
때로는 함수 오버로딩으로 인해 코드가 지저분해질 수 있다는 점을 인정해야 합니다. 반면에 프로그래머는 더 많은 코드를 작성해야 합니다.
어떻게 개선할 수 있습니까?
다음은 JavaScript에서 동일한(거의 동일한) 예제입니다.
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
보시다시피 훨씬 더 선명해 보입니다.
나는 또한 그것에 대한 Elixir 접근 방식을 좋아합니다. 다음은 Elixir에서 어떻게 보일지입니다.
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")
2. 제네릭 부족
이것은 틀림없이 Go 사용자들이 가장 많이 요구하는 기능일 것입니다.
정수 배열과 모든 요소에 적용될 함수를 전달하는 맵 함수를 작성한다고 상상해 보십시오. 쉬워보이죠?
정수에 대해 수행해 보겠습니다.
package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
좋아 보이죠?
글쎄, 당신이 문자열에 대해서도 그것을 할 필요가 있다고 상상해보십시오. 서명을 제외하고 정확히 동일한 다른 구현을 작성해야 합니다. Golang은 함수 오버로딩을 지원하지 않기 때문에 이 함수는 다른 이름이 필요합니다. 결과적으로 이름이 다른 유사한 함수가 많이 생기고 다음과 같이 보일 것입니다.
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
이는 가능한 한 복사/붙여넣기 코드를 작성하고 대신 함수로 이동하여 재사용해야 한다는 DRY(Don't Repeat Yourself) 원칙에 위배됩니다.
또 다른 접근 방식은 interface{}
가 있는 단일 구현을 매개변수로 사용하는 것이지만, 런타임 유형 검사는 오류가 발생하기 쉽기 때문에 런타임 오류가 발생할 수 있습니다. 또한 속도가 더 느려지므로 이러한 기능을 하나로 구현하는 간단한 방법이 없습니다.
어떻게 개선할 수 있습니까?
제네릭 지원을 포함하는 좋은 언어가 많이 있습니다. 예를 들어, 다음은 Rust의 동일한 코드입니다(간단하게 만들기 위해 array
대신 vec
를 사용했습니다).
fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }
map
함수의 단일 구현이 있으며 사용자 정의 유형을 포함하여 필요한 모든 유형에 사용할 수 있습니다.
3. 의존성 관리
Go에 대한 경험이 있는 사람은 종속성 관리가 정말 어렵다고 말할 수 있습니다. Go 도구를 사용하면 사용자가 go get <library repo>
를 실행하여 다른 라이브러리를 설치할 수 있습니다. 여기서 문제는 버전 관리입니다. 라이브러리 관리자가 이전 버전과 호환되지 않는 변경 사항을 일부 변경하고 이를 GitHub에 업로드하면 go get
이 라이브러리 폴더에 저장소를 git clone
하는 것 외에는 아무 작업도 수행하지 않기 때문에 그 이후에 프로그램을 사용하려고 하면 누구든지 오류가 발생합니다. 또한 라이브러리가 설치되어 있지 않으면 프로그램이 컴파일되지 않습니다.

종속성을 관리하기 위해 Dep(https://github.com/golang/dep)를 사용하면 약간 더 잘할 수 있지만 여기서 문제는 모든 종속성을 저장소에 저장한다는 것입니다(저장소가 코드뿐만 아니라 수천 줄의 종속성 코드를 포함하거나 패키지 목록을 저장합니다(그러나 종속성의 유지 관리자가 이전 버전과 호환되지 않는 변경을 하면 모두 충돌합니다).
어떻게 개선할 수 있습니까?
여기에서 완벽한 예는 Node.js(및 일반적으로 JavaScript라고 가정합니다) 및 NPM이라고 생각합니다. NPM은 패키지 저장소입니다. 다른 버전의 패키지를 저장하므로 특정 버전의 패키지가 필요한 경우 문제가 없습니다. 거기에서 가져올 수 있습니다. 또한 모든 Node.js/JavaScript 애플리케이션에 있는 것 중 하나는 package.json
파일입니다. 여기에 모든 종속성과 해당 버전이 나열되어 있으므로 npm install
을 사용하여 모두 설치할 수 있습니다(그리고 코드와 확실히 작동하는 버전을 얻을 수 있음).
또한 패키지 관리의 좋은 예는 RubyGems/Bundler(Ruby 패키지의 경우) 및 Crates.io/Cargo(Rust 라이브러리의 경우)입니다.
4. 오류 처리
Go의 오류 처리는 매우 간단합니다. Go에서는 기본적으로 함수에서 여러 값을 반환할 수 있으며 함수는 오류를 반환할 수 있습니다. 이 같은:
err, value := someFunction(); if err != nil { // handle it somehow }
이제 오류를 반환하는 세 가지 작업을 수행하는 함수를 작성해야 한다고 상상해 보십시오. 다음과 같이 보일 것입니다.
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
여기에는 좋지 않은 반복 가능한 코드가 많이 있습니다. 그리고 기능이 크면 더 나빠질 수 있습니다! 이를 위해 키보드에 키가 필요할 것입니다.
어떻게 개선할 수 있습니까?
나는 그것에 대한 JavaScript의 접근 방식을 좋아합니다. 함수는 오류를 던질 수 있고 잡을 수 있습니다. 다음 예를 고려하십시오.
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
훨씬 더 명확하고 오류 처리를 위한 반복 가능한 코드가 포함되어 있지 않습니다.
바둑의 좋은 점
Go는 설계상 많은 결함이 있지만 몇 가지 정말 멋진 기능도 있습니다.
1. 고루틴
비동기 프로그래밍은 Go에서 정말 간단해졌습니다. 멀티스레딩 프로그래밍은 다른 언어에서 일반적으로 어렵지만 새 스레드를 생성하고 현재 스레드를 차단하지 않도록 함수를 실행하는 것은 정말 간단합니다.
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Go에 번들로 제공되는 도구
다른 프로그래밍 언어에서는 다양한 작업(예: 테스트, 정적 코드 형식 지정 등)에 대해 다른 라이브러리/도구를 설치해야 하지만 다음과 같이 기본적으로 Go에 이미 포함된 멋진 도구가 많이 있습니다.
-
gofmt
- 정적 코드 분석을 위한 도구입니다.eslint
또는jshint
와 같은 추가 종속성을 설치해야 하는 JavaScript와 비교하여 여기에는 기본적으로 포함되어 있습니다. 그리고 Go 스타일 코드를 작성하지 않으면(선언된 변수를 사용하지 않거나 사용하지 않는 패키지를 가져오는 등) 프로그램이 컴파일되지 않습니다. -
go test
- 테스트 프레임워크. 다시 말하지만 JavaScript와 비교하여 테스트를 위해 추가 종속성을 설치해야 합니다(Jest, Mocha, AVA 등). 여기서는 기본적으로 포함됩니다. 그리고 기본적으로 벤치마킹, 문서의 코드를 테스트로 변환하는 등 많은 멋진 작업을 수행할 수 있습니다. -
godoc
- 문서화 도구. 기본 도구에 포함되어 있어 좋습니다. - 컴파일러 자체. 다른 컴파일된 언어와 비교할 때 엄청나게 빠릅니다!
3. 연기하다
나는 이것이 언어에서 가장 좋은 기능 중 하나라고 생각합니다. 세 개의 파일을 여는 함수를 작성해야 한다고 상상해 보십시오. 그리고 문제가 발생하면 기존에 열려 있는 파일을 닫아야 합니다. 그런 구조가 많으면 엉망이 될 것입니다. 다음 의사 코드 예제를 고려하십시오.
function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
복잡해 보인다. 이것이 Go의 defer
이 발생하는 곳입니다.
package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
보시다시피 3번 파일을 열 때 오류가 발생하면 defer
문이 역순으로 반환되기 전에 실행되기 때문에 다른 파일은 자동으로 닫힙니다. 또한 함수의 다른 부분이 아닌 같은 위치에서 파일을 열고 닫는 것이 좋습니다.
결론
나는 바둑의 좋은 점과 나쁜 점을 모두 언급하지 않고 내가 최고와 최악이라고 생각하는 것들만 언급했다.
Go는 실제로 현재 사용 중인 흥미로운 프로그래밍 언어 중 하나이며 실제로 잠재력이 있습니다. 정말 멋진 도구와 기능을 제공합니다. 그러나 거기에는 개선할 수 있는 것들이 많이 있습니다.
Go 개발자로서 우리가 이러한 변경 사항을 구현하면 Go로 프로그래밍하는 것이 훨씬 더 즐거워지기 때문에 커뮤니티에 많은 도움이 될 것입니다.
그동안 Go를 사용하여 테스트를 개선하려는 경우 동료 Toptaler Gabriel Aszalos가 작성한 Test Your Go App: Get Started Right Way 를 시도하십시오.