Record

개념

  • C, C++, Go, D와 같은 컴파일 기반의 언어이자 시스템 프로그래밍 언어에 속하며, Go보다는 반 년 늦게 나왔지만[1] 그나마 비슷한 시기에 등장했다는 점과 두 언어 모두 C/C++를 서로 다른 방향에서 대체하려 한다는 점 때문에 라이벌 관계로 엮이기도 한다. 멀티코어 프로세싱이 중요시되는 현 추세에 따라 동시성 프로그래밍 및 병렬 프로그래밍에도 강점을 가지고 있다

특징

  • Rust는 현대적인 시스템 프로그래밍 언어로, C/C++와 동등한 수준의 속도를 달성하면서 메모리 오류를 완전히 없애는 것을 목표로 한다. 또한 함수형 프로그래밍 언어로부터 발전된 타입 시스템을 도입하였으며, 클래스 대신 트레이트(Trait)를 기반으로 다형성을 달성한다. 매크로를 사용해 언어를 확장하는 것이 가능하며, 이 모든 것이 현대적인 모듈 시스템을 통해 쉽게 모듈화될 수 있다. 모듈들은 크레이트(Crate)라고 하는 단위로 묶여서 실행 파일이나 라이브러리로 배포될 수 있으며, Cargo라는 패키지 관리 프로그램을 통해 빌드 및 패키지 배포를 자동화하고 필요한 라이브러리를 Cargo를 통해 자동으로 다운로드받을 수 있다.

안전한 메모리 관리

  • Rust는 “쓰레기 수집 없이 메모리 안전성을 제공하는 언어”다. 다시 말해, Java처럼 바이트코드 실행기를 돌리거나, Python 등의 스크립트 언어들처럼 인터프리터 런타임을 돌리지 않고 C/C++와 동등한 기계어 코드를 만들어내면서 메모리 오류는 컴파일 시간에 잡아낸다는 것이다.
  • 소유권
  • Mutability

제네릭(Generic)과 트레이트(Trait) 제공

  • C++, C#, Java 등 대중적인 정적 타입 프로그래밍 언어들에서 흔히 제공하는 제네릭을 Rust 또한 가지고 있다. C#이나 Java처럼 타입 인자를 제공하는 정도의 기능을 갖고 있으며, 내부적으로는 C++의 템플릿처럼 타입별로 코드를 생성하는 방식으로 동작한다. 코드 생성에 필요한 정보를 라이브러리에 포함시키기 때문에 C++와는 달리 외부 라이브러리에 포함된 제네릭 타입이나 함수도 제약 없이 쓸 수 있다.
  • 특수한 Trait
  • 하이지닉 매크로

Rust 컴파일

  • rustc main.rs

Rust 패키지 매니저 Cargo 프로젝트 생성

  • cargo new RustApp –bin
  • cargo build

GOPATH

  • Go는 표준 패키지 이외의 3rd Party 패키지나 사용자 정의 패키지들을 이 GOPATH 에서 찾는다. 복수 개의 경로를 지정한 경우, 3rd Party 패키지는 처음 경로에 설치된다.

주요 명령어

  • go run src/hello.go
  • go build test.go

주요 문법

포인터연산자

var k int = 10
var p = &k  //k의 주소를 할당
println(*p) //p가 가리키는 주소에 있는 실제 내용을 출력

for range 문

names := []string{"홍길동", "이순신", "강감찬"}
for index, name := range names {
    println(index, name)
}

break, continue, goto 문

  • for 루프를 빠져나와 L1 레이블로 이동한 후, break가 있는 현재 for 루프를 건너뛰고 다음 문장인 println() 으로 이동
package main

func main() {
    i := 0

L1:
    for {

        if i == 0 {
            break L1
        }
    }

    println("OK")
}

Variadic Function 가변인자함수

  • 문자열 가변 파라미터를 나타내기 위해서 …string 과 같이 표현
package main
func main() {
    say("This", "is", "a", "book")
    say("Hi")
}

func say(msg ...string) {
    for _, s := range msg {
        println(s)
    }
}

함수 리턴값

package main

func main() {
    count, total := sum(1, 7, 3, 5, 9)
    println(count, total)
}

//리턴값 복수
func sum(nums ...int) (int, int) {
    s := 0      // 합계
    count := 0  // 요소 갯수
    for _, n := range nums {
        s += n
        count++
    }
    return count, s
}

// Named Return Parameter = count int, total int
func sum(nums ...int) (count int, total int) {
    for _, n := range nums {
        total += n
    }
    count = len(nums)
    return
}

익명함수

  • 함수명을 갖지 않는 함수를 익명함수(Anonymous Function)

일급함수

  • Go 프로그래밍 언어에서 함수는 일급함수로서 Go의 기본 타입과 동일하게 취급되며, 따라서 다른 함수의 파라미터로 전달하거나 다른 함수의 리턴값으로도 사용될 수 있다
package main

func main() {
    //변수 add 에 익명함수 할당
    add := func(i int, j int) int {
        return i + j
    }

    // add 함수 전달
    r1 := calc(add, 10, 20)
    println(r1)

    // 직접 첫번째 파라미터에 익명함수를 정의함
    r2 := calc(func(x int, y int) int { return x - y }, 10, 20)
    println(r2)

}

func calc(f func(int, int) int, a int, b int) int {
    result := f(a, b)
    return result
}

type문을 사용한 함수 원형 정의

  • 이렇게 함수의 원형을 정의하고 함수를 타 메서드에 전달하고 리턴받는 기능을 타 언어에서 흔히 델리게이트(Delegate)라 부른다. Go는 이러한 Delegate 기능을 제공하고 있다
// 원형 정의
type calculator func(int, int) int

// calculator 원형 사용
func calc(f calculator, a int, b int) int {
    result := f(a, b)
    return result
}

클로저 (Closure)

  • Go 언어에서 함수는 Closure로서 사용될 수도 있다. Closure는 함수 바깥에 있는 변수를 참조하는 함수값(function value)를 일컫는데, 이때의 함수는 바깥의 변수를 마치 함수 안으로 끌어들인 듯이 그 변수를 읽거나 쓸 수 있게 된다.
package main

func nextValue() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {
    next := nextValue()

    println(next())  // 1
    println(next())  // 2
    println(next())  // 3

    anotherNext := nextValue()
    println(anotherNext()) // 1 다시 시작
    println(anotherNext()) // 2
}

슬라이스(Slice)

  • 슬라이스의 길이(Length)와 용량(Capacity)
func main() {
    s := make([]int, 5, 10)
    println(len(s), cap(s)) // len 5, cap 10
}

// 슬라이스에 별도의 길이와 용량을 지정하지 않으면,
기본적으로 길이와 용량이 0 인 슬라이스를 만드는데,
이를 Nil Slice 라 하고, nil 과 비교하면 참을 리턴한다.

func main() {
    var s []int

    if s == nil {
        println("Nil Slice")
    }
    println(len(s), cap(s)) // 모두 0
}
  • 부분 슬라이스(Sub-slice)
s := []int{0, 1, 2, 3, 4, 5}
s = s[2:5]     // 2, 3, 4
s = s[1:]      // 3, 4
fmt.Println(s) // 3, 4 출력
  • 슬라이스 추가,병합(append)과 복사(copy)
package main

import "fmt"

func main() {
    // len=0, cap=3 인 슬라이스
    sliceA := make([]int, 0, 3)

    // 계속 한 요소씩 추가
    for i := 1; i <= 15; i++ {
        sliceA = append(sliceA, i)
        // 슬라이스 길이와 용량 확인
        fmt.Println(len(sliceA), cap(sliceA))
    }

    fmt.Println(sliceA) // 1 부터 15 까지 숫자 출력
}

func main() {
    source := []int{0, 1, 2}
    target := make([]int, len(source), cap(source)*2)
    copy(target, source)
    fmt.Println(target)  // [0 1 2 ] 출력
    println(len(target), cap(target)) // 3, 6 출력
}

Map 개요

tickers := map[string]string{
    "GOOG": "Google Inc",
    "MSFT": "Microsoft",
    "FB":   "FaceBook",
}

package main

func main() {
    tickers := map[string]string{
        "GOOG": "Google Inc",
        "MSFT": "Microsoft",
        "FB":   "FaceBook",
        "AMZN": "Amazon",
    }

    // map 키 체크
    val, exists := tickers["MSFT"]
    if !exists {
        println("No MSFT ticker")
    }

    for key, val := range tickers {
        fmt.Println(key, val)
    }
}

패키지 Scope

  • 패키지 내에는 함수, 구조체, 인터페이스, 메서드 등이 존재하는데, 이들의 이름(Identifier)이 첫문자를 대문자로 시작하면 이는 public 으로 사용할 수 있다. 즉, 패키지 외부에서 이들을 호출하거나 사용할 수 있게 된다. 반면, 이름이 소문자로 시작하면 이는 non-public 으로 패키지 내부에서만 사용될 수 있다.

패키지 init 함수와 alias

package testlib

var pop map[string]string

func init() {   // 패키지 로드시 map 초기화
    pop = make(map[string]string)
}

package main
import _ "other/xlib" //그 패키지 안의 init() 함수만을 호출하고자 하는 케이스

// 패키지 alias를 사용해서 구분
import (
    mongo "other/mongo/db"
    mysql "other/mysql/db"
)
func main() {
    mondb := mongo.Get()
    mydb := mysql.Get()
    //...
}

Struct (구조체)

package main

import "fmt"

// struct 정의
type person struct {
    name string
    age  int
}

func main() {
    // person 객체 생성
    p := person{}

    // 필드값 설정
    p.name = "Lee"
    p.age = 10

    fmt.Println(p)
}

Go 메서드(Method)

package main

//Rect - struct 정의
type Rect struct {
    width, height int
}

//Rect의 area() 메소드
func (r Rect) area() int {
    return r.width * r.height
}

func main() {
    rect := Rect{10, 20}
    area := rect.area() //메서드 호출
    println(area)
}

Go 인터페이스

type Shape interface {
    area() float64
    perimeter() float64
}


//Rect 정의
type Rect struct {
    width, height float64
}

//Circle 정의
type Circle struct {
    radius float64
}

//Rect 타입에 대한 Shape 인터페이스 구현
func (r Rect) area() float64 { return r.width * r.height }
func (r Rect) perimeter() float64 {
     return 2 * (r.width + r.height)
}

//Circle 타입에 대한 Shape 인터페이스 구현
func (c Circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c Circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

인터페이스 타입

  • 빈 interface는 interface{} 와 같이 표현한다.
func Marshal(v interface{}) ([]byte, error);

func Println(a ...interface{}) (n int, err error);

// 인터페이스는 어떠한 타입도 담을 수 있는 컨테이너라고 볼 수 있으며,
여러 다른 언어에서 흔히 일컫는 Dynamic Type 이라고 볼 수 있다.
(주: empty interface는 C#, Java 에서 object라 볼 수 있으며,
C/C++ 에서는 void* 와 같다고 볼 수 있다)
  • Type Assertion
// Interface type의 x와 타입 T에 대하여 x.(T)로 표현했을 때,
이는 x가 nil이 아니며, x는 T 타입에 속한다는 점을 확인(assert)하는 것으로
이러한 표현을 "Type Assertion"이라 부른다.

func main() {
    var a interface{} = 1

    i := a       // a와 i 는 dynamic type, 값은 1
    j := a.(int) // j는 int 타입, 값은 1

    println(i)  // 포인터주소 출력
    println(j)  // 1 출력
}

Go 에러

  • Go는 내장 타입으로 error 라는 interface 타입을 갖는다
type error interface {
    Error() string
}
  • Go 함수가 결과와 에러를 함께 리턴한다면, 이 에러가 nil 인지를 체크해서 에러가 없는지를 체크할 수 있다
package main

import (
    "log"
    "os"
)

func main() {
    f, err := os.Open("C:\\temp\\1.txt")
    if err != nil {
        log.Fatal(err.Error())
    }
    println(f.Name())
}
  • error의 Type을 체크해서 에러 타입별로 별도의 에러 처리를 하는 방식
_, err := otherFunc()
switch err.(type) {
default: // no error
    println("ok")
case MyError:
    log.Print("Log my error")
case error:
    log.Fatal(err.Error())
}

지연실행 defer

  • 차후 문장에서 어떤 에러가 발생하더라도 항상 파일을 Close할 수 있도록 한다.
package main

import "os"

func main() {
    f, err := os.Open("1.txt")
    if err != nil {
        panic(err)
    }

    // main 마지막에 파일 close 실행
    defer f.Close()

    // 파일 읽기
    bytes := make([]byte, 1024)
    f.Read(bytes)
    println(len(bytes))
}

panic 함수

  • Go 내장함수인 panic()함수는 현재 함수를 즉시 멈추고 현재 함수에 defer 함수들을 모두 실행한 후 즉시 리턴한다.
package main

import "os"

func main() {
    openFile("Invalid.txt")
    println("Done") //이 문장은 실행 안됨
}

func openFile(fn string) {
    f, err := os.Open(fn)
    if err != nil {
        panic(err)
    }
    // 파일 close 실행됨
    defer f.Close()
}

recover 함수

  • Go 내장함수인 recover()함수는 panic 함수에 의한 패닉상태를 다시 정상상태로 되돌리는 함수이다
package main

import (
    "fmt"
    "os"
)

func main() {
    openFile("1.txt")
    println("Done") // 이 문장 실행됨
}

func openFile(fn string) {
    // defere 함수. panic 호출시 실행됨
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("OPEN ERROR", r)
        }
    }()

    f, err := os.Open(fn)
    if err != nil {
        panic(err)
    }

    // 파일 close 실행됨
    defer f.Close()
}

Go루틴

  • Go루틴(goroutine)은 Go 런타임이 관리하는 Lightweight 논리적 (혹은 가상적) 쓰레드
// goroutine은 OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent 처리를
구현하기 위하여 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다.
Go 런타임 상에서 관리되는 작업단위인 여러 goroutine들은
종종 하나의 OS 쓰레드 1개로도 실행되곤 한다.
즉, Go루틴들은 OS 쓰레드와 1 대 1로 대응되지 않고,
Multiplexing으로 훨씬 적은 OS 쓰레드를 사용한다.
메모리 측면에서도 OS 쓰레드가 1 메가바이트의 스택을 갖는 반면,
goroutine은 이보다 훨씬 작은 몇 킬로바이트의 스택을 갖는다(필요시 동적으로 증가).
Go 런타임은 Go루틴을 관리하면서 Go 채널을 통해 Go루틴 간의 통신을 쉽게 할 수 있도록 하였다.
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 10; i++ {
        fmt.Println(s, "***", i)
    }
}

func main() {
    // 함수를 동기적으로 실행
    say("Sync")

    // 함수를 비동기적으로 실행
    go say("Async1")
    go say("Async2")
    go say("Async3")

    // 3초 대기
    time.Sleep(time.Second * 3)
}

익명함수 Go루틴

package main

import (
    "fmt"
    "sync"
)

func main() {
    // WaitGroup 생성. 2개의 Go루틴을 기다림.
    // sync.WaitGroup을 사용하고 있는데,
    이는 기본적으로 여러 Go루틴들이 끝날 때까지 기다리는 역활

    var wait sync.WaitGroup
    wait.Add(2)

    // 익명함수를 사용한 goroutine
    go func() {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println("Hello")
    }()

    // 익명함수에 파라미터 전달
    go func(msg string) {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println(msg)
    }("Hi")

    wait.Wait() //Go루틴 모두 끝날 때까지 대기
}

다중 CPU 처리

  • Go 프로그램을 다중 CPU에서 병렬처리 (Parallel 처리)하게 할 수 있는데, 병렬처리를 위해서는 아래와 같이 runtime.GOMAXPROCS(CPU수) 함수를 호출하여야 한다
package main

import (
    "runtime"
)

func main() {
    // 4개의 CPU 사용
    runtime.GOMAXPROCS(4)

    //...
}

Go 채널

  • Go 채널은 그 채널을 통하여 데이타를 주고 받는 통로라 볼 수 있는데, 채널은 make() 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 을 통해 데이타를 보내고 받는다. 채널은 흔히 goroutine들 사이 데이타를 주고 받는데 사용되는데, 상대편이 준비될 때까지 채널에서 대기함으로써 별도의 lock을 걸지 않고 데이타를 동기화하는데 사용된다.
package main

func main() {
  // 정수형 채널을 생성한다
  ch := make(chan int)

  go func() {
    ch <- 123   //채널에 123을 보낸다
  }()

  var i int
  i = <- ch  // 채널로부터 123을 받는다
  println(i)
}
  • Go 채널은 수신자와 송신자가 서로를 기다리는 속성때문에, 이를 이용하여 (다음 예제와 같이) Go루틴이 끝날 때까지 기다리는 기능을 구현할 수 있다. 익명함수 Go 루틴에서 작업이 끝난 후, done채널에 true를 보내면, 수신자 메인루틴은 이를 받고 프로그램을 끝내게 된다.
package main

import "fmt"

func main() {
    done := make(chan bool)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        done <- true
    }()

    // 위의 Go루틴이 끝날 때까지 대기
    <-done
}

Go 채널 버퍼링

  • Buffered Channel을 사용하면 비록 수신자가 받을 준비가 되어 있지 않을 지라도 지정된 버퍼만큼 데이타를 보내고 계속 다른 일을 수행할 수 있다
package main

import "fmt"

func main() {
  c := make(chan int)
  c <- 1   //수신루틴이 없으므로 데드락
  fmt.Println(<-c) //코멘트해도 데드락 (별도의 Go루틴없기 때문)
}


package main

import "fmt"

func main() {
    ch := make(chan int, 1)

    //수신자가 없더라도 보낼 수 있다.
    ch <- 101

    fmt.Println(<-ch)
}

채널 파라미터

  • 특별히 해당 채널로 송신만 할 것인지 혹은 수신만할 것인지를 지정할 수도 있다
package main

import "fmt"

func main() {
    ch := make(chan string, 1)
    sendChan(ch)
    receiveChan(ch)
}

func sendChan(ch chan<- string) {
    ch <- "Data"
    // x := <-ch // 에러발생
}

func receiveChan(ch <-chan string) {
    data := <-ch
    fmt.Println(data)
}

채널 닫기

  • 채널을 오픈한 후 데이타를 송신한 후, close()함수를 사용하여 채널을 닫을 수 있다. 채널을 닫게 되면, 해당 채널로는 더이상 송신을 할 수 없지만, 채널이 닫힌 이후에도 계속 수신은 가능하다
package main

func main() {
    ch := make(chan int, 2)

    // 채널에 송신
    ch <- 1
    ch <- 2

    // 채널을 닫는다
    close(ch)

    // 채널 수신
    println(<-ch)
    println(<-ch)

    if _, success := <-ch; !success {
        println("더이상 데이타 없음.")
    }
}

채널 range 문

  • 채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있다. 그리고 수신자는 임의의 갯수의 데이타를 채널이 닫힐 때까지 계속 수신할 수 있다. 채널 range문은 range 키워드 다음의 채널로부터 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료한다.
package main

func main() {
    ch := make(chan int, 2)

    // 채널에 송신
    ch <- 1
    ch <- 2

    // 채널을 닫는다
    close(ch)

    // 방법1
    // 채널이 닫힌 것을 감지할 때까지 계속 수신
    /*
    for {
        if i, success := <-ch; success {
            println(i)
        } else {
            break
        }
    }
    */

    // 방법2
    // 위 표현과 동일한 채널 range 문
    for i := range ch {
        println(i)
    }
}

채널 select 문

  • Go의 select문은 복수 채널들을 기다리면서 준비된 (데이타를 보내온) 채널을 실행하는 기능을 제공한다
첫번째 run1()이 1초간 실행되고 done1 채널로부터 수신하여 해당 case를 실행하고,
다시 for 루프를 돈다.
for루프를 다시 돌면서 다시 select문이 실행되는데,
다음 run2()가 2초후에 실행되고 done2 채널로부터 수신하여 해당 case를 실행하게 된다.
done2 채널 case문에 break EXIT 이 있는데,
이 문장으로 인해 for 루프를 빠져나와 EXIT 레이블로 이동하게 된다

package main

import "time"

func main() {
    done1 := make(chan bool)
    done2 := make(chan bool)

    go run1(done1)
    go run2(done2)

EXIT:
    for {
        select {
        case <-done1:
            println("run1 완료")

        case <-done2:
            println("run2 완료")
            break EXIT
        }
    }
}

func run1(done chan bool) {
    time.Sleep(1 * time.Second)
    done <- true
}

func run2(done chan bool) {
    time.Sleep(2 * time.Second)
    done <- true
}