JavaScript 개발자를 위한 Golang - 2부(번역)
Golang for JavaScript developers - Part 2
출처
Golang for JavaScript developers - Part 2
시작
만약 당신이 다른 프로그래밍 언어를 배우는 것에 대해 생각하고 있는 Javascript 개발자라면, Golang은 훌륭한 선택이다. 단순하며 많은 모멘텀과 좋은 성능 그리고 JavaScript와 일부 유사하다.
이 포스트는 언어를 비교하는 것이 아니고 그들이 매우 유사하다고 말하고 있다. 이것은 JavaScript 개발자가 Golang을 빨리 파악할 수 있는 가이드이다. Go에는 우리가 다룰 자바스크립트와 완전히 다른 많은 측면들이 있다.
이 시리즈의 이전 편에서, 우리는 JS와 Go 사이에 유사한 것들에 대해 배웠다. 배운 것들:
- Functions
- 스코프(Scope)
- 흐름 제어(Flow control)
- 메모리 관리(Memory management)
이편에서는 JS와 Go의 차이점을 다룰 것이다. 만약 이전 편을 읽어보지 않았다면 먼저 읽어보자.
더 다른 것들
보다시피 이 편에는 이전보다 더 많은 내용이 있지만 몇 가지 차이점들은 미묘해서 JavaScript 개발자는 이해하기 쉬울 것이다.
타입 & 변수(Types & Variables)
이것은 주요 차이점 중 하나다. JavaScript는 동적이고 느슨하게 타입되고 Go는 정적이고 엄격하게 타입된다.
JavaScript
var foo = {
message: "hello"
};
var bar = foo;
// 가변
bar.message = "world";
console.log(foo.message === bar.message); // prints 'true'
// 재할당
bar = {
message: "mars"
};
console.log(foo.message === bar.message); // prints 'false'
Go
var foo = struct {
message string
}{"hello"}
var bar = foo // foo의 복사본을 만들고 bar에 할당한다
// bar만 변경
// 참고 bar.message는 (* bar) .message의 약어
bar.message = "world"
fmt.Println(foo.message == bar.message) // prints "false"
// bar 재할당
bar = struct {
message string
}{"mars"}
fmt.Println(foo.message == bar.message) // prints "false"
var barPointer = &foo // foo에 포인터를 할당
// foo를 변경
barPointer.message = "world"
fmt.Println(foo.message == barPointer.message) // prints "true"
// foo를 재할당
*barPointer = struct {
message string
}{"mars"}
fmt.Println(foo.message == bar.message) // prints "true"
유사점
- 키워드
var
와const
의 이름 외에는 유사성이 없다. Go의var
키워드는 동작 측면에서 JS의let
키워드와 비슷하다. var a, foo, bar int
과 같이 여러var
를 함께 선언 할 수 있는데 JS와 유사하다. 그러나 Go에서는var a, foo, bar = true, 10, "hello"
와 같이 더 나아가서 초기화 할 수 있다. JS에서는var [a, foo, bar] = [true, 10, "hello"]
와 같은 유사한 효과에 대해 구조 분해 할당(destructuring assignment)을 할 수 있다.
차이점
- Go는 컴파일 시 지정된 타입(specified type) 또는 타입 추론(type inference)으로 타입 정보를 필요로 한다.
- Go에는 value 타입(primitives, arrays, and structs), reference 타입(slice, map & channels) 그리고 포인터가 있다. JS는 value 타입(primitives)과 reference 타입(objects, arrays, functions)을 가지고 있다.
- Go에서 선언 한 후 변수 타입을 변경할 수 없다.
- 변수 할당은 Go에서 단락 표현식(short-circuit expressions)을 사용할 수 없다.
var
는 Go functions에: =
와 같은 간단한 문법이 있다.- Go는 엄격하게 사용하지 않는 변수를 허용하지 않는다. 사용되지 않은 변수는 예약 된 문자 인
_
로 이름을 지정해야한다. - Go에서
const
는 JavaScript와 같지 않다. Go에선 character, string, boolean 또는 numeric values과 같은 원시값(primitives)만 지정할 수 있다. - Go의 Arrays는 고정 길이이므로 JS와 다르다. JS Arrays는 동적이므로 동적 길이를 가진 array의 slices 인 Go slices와 더 유사하다.
JavaScript
const foo = ["Rick", "Morty"];
// array의 끝에 추가한다.
foo.push("Beth");
// array의 끝에서 제거한다.
element = foo.pop();
Go
foo := []string{"Rick", "Morty"} // slice를 만든다
// array의 끝에 추가한다.
foo = append(foo, "Beth")
// array의 끝에서 제거한다.
n := len(foo) - 1 // 마지막 요소의 index
element := foo[n] // 선택적으로 마지막 요소를 가져온다
foo = foo[:n] // 마지막 요소를 제거한다
- JavaScript에는 dictionaries와 sets로 사용할 수있는 Object, Map / Set 및 WeakMap / WeakSet이 있다. Go에는 JavaScript Object와 더 유사한 단순한 Map 만 이런 이유로 그 목적에 부합한다. 또한 Go에서 maps는 순서가 없다.(not ordered)
Javascript
const dict = {
key1: 10,
key2: "hello"
};
const stringMap = {
key1: "hello",
key2: "world"
};
Go
var dict = map[string]interface{}{
"key1": 10,
"key2": "hello",
}
var stringMap = map[string]string{
"key1": "hello",
"key2": "world",
}
가변성(Mutability)
JS와 Go의 또 다른 주요 차이점은 변수 가변(variable mutations)이 처리되는 방식이다. JavaScript에서 모든 non-primitive 변수는 참조(reference)로 전달된다. 그리고 그 행동을 바꿀 방법은 없다. 반면에 Go에서는 slice, map & channels를 제외한 모든 것이 값으로 전달되고 그 대신 포인터를 변수에 명시적으로 전달하여 이를 변경하도록 선택할 수 있다.
Go에선 이 때문에 JS보다 가변성을 더 잘 제어 할 수 있다.
Javascript에서 또 다른 주목할만한 차이점은 Go에서는 불가능한 const
키워드를 사용하여 변수의 재할당을 방지 할 수 있다.
위의 섹션에서 약간의 변경 가능성이 있음을 보았다. 좀 더 살펴보자.
JavaScript
let foo = {
msg: "hello"
};
function mutate(arg) {
arg.msg = "world";
}
mutate(foo);
console.log(foo.msg); // prints 'world'
Go
type Foo struct {
msg string
}
var foo = Foo{"hello"}
var tryMutate = func(arg Foo) {
arg.msg = "world"
}
tryMutate(foo)
fmt.Println(foo.msg) // prints 'hello'
var mutate = func(arg *Foo) {
arg.msg = "world"
}
mutate(&foo)
fmt.Println(foo.msg) // prints 'world'
에러 처리(Error handling)
에러 처리 측면에서 Go와 JS 사이의 유일한 유사점은 에러도 단지 값 타입(value types)이라는 것이다. 두 언어 다 모두 에러를 값으로 전달할 수 있다.
위의 에러 처리 외에도 두 가지 모두 상당히 다르다. JavaScript에서 우리는 둘 중 하나를 할 수 있다.
try / catch
메커니즘을 사용하여async / await
를 쓰는 동기 함수(synchronous functions) 및 비동기 함수(asynchronous functions)에서 에러를 파악- 콜백 함수(callback functions)에 전달하거나 비동기 함수에 대해 promises를 사용하여 에러를 처리
Go에서는 try / catch
메커니즘이 없으며 에러를 처리하는 유일한 방법은 에러를 function에서 value로 반환하거나 panic
으로 실행을 중지하는 것이다.
이것은 Go에서 에러 처리를 매우 장황하게 만들고 Go에서 유명한 if err != nil
문을 자주 보게한다.
JavaScript
function errorCausingFunction() {
throw Error("Oops");
}
try {
errorCausingFunction();
} catch (err) {
console.error(`Error: ${err}`);
} finally {
console.log(`Done`);
}
// prints
// Error: Error: Oops
// Done
// or the async way
function asyncFn() {
try {
errorCausingFunction();
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
asyncFn()
.then(res => console.log(`:)`))
.catch(err => console.error(`Error: ${err}`))
.finally(res => console.log(`Done`));
// prints
// Error: Error: Oops
// Done
Go
var errorCausingFunction = func() error {
return fmt.Errorf("Oops")
}
err := errorCausingFunction()
defer fmt.Println("Done")
// 마지막에 가장 가깝지만, enclosing function의 끝날 때만 실행
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
} else {
fmt.Println(":)")
}
// prints
// Error: Oops
// Done
상속 대신 합성(Composition instead of inheritance)
JavaScript에서 상속(inheritance)을 사용하여 동작을 확장하거나 공유 할 수 있으며 Go는 합성(composition)을 대신 선택한다. JavaScript에는 프로토 타입 레벨 상속이 있고 언어의 유연성으로 인해 합성이 가능하다.
JavaScript
class Animal {
species;
constructor(species) {
this.species = species;
}
species() {
return this.species;
}
}
class Person extends Animal {
name;
constructor(name) {
super("human");
this.name = name;
}
name() {
return this.name;
}
}
var tom = new Person("Tom");
console.log(`${tom.name} is a ${tom.species}`); // prints 'Tom is a human'
Go
type IAnimal interface {
Species() string
}
type IPerson interface {
IAnimal
// IAnimal 인터페이스 합성
Name() string
}
type Animal struct {
species string
}
type Person struct {
Animal
// Animal struct의 합성
name string
}
func (p *Person) Name() string {
return p.name
}
func (p *Animal) Species() string {
return p.species
}
func NewPerson(name string) IPerson {
return &Person{Animal{"human"}, name}
}
func main() {
var tom IPerson = NewPerson("Tom")
fmt.Printf("%s is a %s\n", tom.Name(), tom.Species()) // prints 'Tom is a human'
}
동시성(Concurrency)
동시성(Concurrency)은 Golang의 가장 중요한 기능 중 하나이며 빛나는 부분이다.
JavaScript는 기술적으로 단일 스레드이므로 실제 기본 동시성(real native concurrency)이 없다.
service workers의 추가는 병렬 처리에 대한 약간의 지원을 제공하지만 여전히 goroutines
의 성능과 단순성에 필적할 수는 없다.
동시성은 JavaScript가 크게 지원하는 비동기식(asynchronous) 또는 반응형 프로그래밍(reactive programming)과 같지 않다.
// 순차적
async function fetchSequential() {
const a = await fetch("http://google.com/");
console.log(a.status);
await a.text();
const b = await fetch("http://twitter.com/");
console.log(b.status);
await b.text();
}
// 동시적이지만 다중 스레드 아님
async function fetchConcurrent() {
const values = await Promise.all([
fetch("http://google.com/"),
fetch("http://twitter.com/")
]);
values.forEach(async resp => {
console.log(resp.status);
await resp.text();
});
}
반면에 Go는 동시성과 병렬 처리에 완전히 맞춰져 있다.
개념은 goroutines
과 channels를 사용하여 언어에 내장되어 있다.
Go에서 비동기 프로그래밍을 수행하는 것도 가능하지만 JS와 동등한 것 보다 더 장황하게 보인다.
이것은 goroutines를 사용하여 API를 동기화로 작성하고 비동기 방식으로 사용할 수 있다는 것을 의미하며 Go 커뮤니티는 일반적으로 비동기 API 작성을 반대한다.
// 순차적
func fetchSequential() {
respA, _ := http.Get("http://google.com/")
defer respA.Body.Close()
fmt.Println(respA.Status)
respB, _ := http.Get("http://twitter.com/")
defer respB.Body.Close()
fmt.Println(respB.Status)
}
// 동시 및 다중 스레드
func fetchConcurrent() {
resChanA := make(chan *http.Response, 0)
go func(c chan *http.Response) {
res, _ := http.Get("http://google.com/")
c <- res
}(resChanA)
respA := <-resChanA
defer respA.Body.Close()
fmt.Println(respA.Status)
resChanB := make(chan *http.Response, 0)
go func(c chan *http.Response) {
res, _ := http.Get("http://twitter.com/")
c <- res
}(resChanB)
respB := <-resChanB
defer respB.Body.Close()
fmt.Println(respB.Status)
}
컴파일(Compilation)
JavaScript는 해석이 되며(interpreted) 컴파일은 되지(compiled) 않는다. 일부 JS 엔진은 JIT 컴파일을 사용하지만 개발자에게는 JavaScript를 실행하기 위해 컴파일 할 필요가 없으므로 중요하지 않다. TypeScript 또는 Babel을 사용한 코드 변환(Transpiling)은 계산되지 않는다 😉
Go는 컴파일되며 그 때문에 compile-time type의 안전성과 어느 정도의 메모리 안전성을 제공한다.
패러다임
JavaScript는 주로 객체 지향적이지만 언어의 유연성으로 때문에 명령형(imperative) 또는 함수형-스타일(functional-style) 코드를 쉽게 작성할 수 있다. 이 언어는 상당히 자유로운 형태이며 실제로 아무 것도 강요하지 않는다. 이것은 편향적(opinionated)이지 않으며 즉시 사용 가능한 어떤 틀(tooling)도 제공하지 않는다. 개발자는 그/그녀 자신의 틀(tooling)을 설정할 필요가 있을 것이다.
Go는 기본적으로 명령형이며 약간의 OOP(Object Oriented Programming 객체 지향 프로그래밍) 및 함수형을 수행 할 수 있지만, JavaScript에서처럼 쉽지는 않다. 이 언어는 상당히 엄격하고 편향적이며(opinionated) 코드 스타일 및 포맷팅과 같은 것을 시행한다. 또한 테스팅, 포맷팅, 빌딩 등을 위한 내장 기능도 제공한다.
결론
어떤 사람이 시리즈의 이전 편 댓글에서 JS 개발자가 가능한 모든 옵션 중에서 왜 Go를 선택해야 하는지 물었다. 내 생각에, JS는 완벽한 언어가 아니기 때문에 다른 언어를 배우면 JS 개발자가 JS를 더 실용적으로 사용할 수 있게 큰 도움이 될 것이고 기본적인 프로그래밍 개념에 대한 그녀/그의 지식을 강화하는 데도 도움이 될 것이다. 물론 Rust, Go, Haskel, Kotlin 등과 같은 많은 옵션들이 있지만, 나는 Go가 사용 가능한 모든 옵션 중에서 가장 단순하고 널리 채택되어 시작하기에 좋은 것이라고 생각한다. 나의 두번째 선택은 Kotlin이나 Rust가 될 것이다.
참조:
- http://www.pazams.com/Go-for-Javascript-Developers/
- https://github.com/miguelmota/golang-for-nodejs-developers
마치며
부족한 영어 실력이나마 매끄럽게 번역을 하기 위해 직역 및 의역을 다수 사용했다.
(문장의 해석이 오역이거나 수정이 필요한 점이 있다면 언제든 알려주시면 반영하겠습니다.)
- function은 함수라 표기하지 않고 영문 그대로 표기했다.