logo

JavaScript 개발자를 위한 Golang - 2부(번역)

Golang for JavaScript developers - Part 2

javascript, go, translation


출처

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"

유사점

  • 키워드 varconst의 이름 외에는 유사성이 없다. 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가 될 것이다.

참조:

마치며

부족한 영어 실력이나마 매끄럽게 번역을 하기 위해 직역 및 의역을 다수 사용했다.

(문장의 해석이 오역이거나 수정이 필요한 점이 있다면 언제든 알려주시면 반영하겠습니다.)

  • function은 함수라 표기하지 않고 영문 그대로 표기했다.