[GO 마스터하기] 01-Go 언어와 운영 체제
Go 언어를 배우는 이유
G는 최신 프로그래밍 언어로 자잘한 버그가 발생할 걱정 없이 코드를 안전하게 작성할 수 있다. Go 언어는 많은 개발자들을 즐겁게 해주기 위한 목적으로 설계되었기 때문에, 코드를 작성하기 쉽다.
Go 언어 장점
- Go 코드는 읽고 이해하기 쉽다
- 최대한 개발자를 즐겁게 해주려고 한다.
- 컴파일러는 문제 해결에 꼭 필요한 경고와 에러 메시만 출력한다.
- 이식성이 뛰어나다
- 절차형, 동시성, 분산 프로그래밍을 지원한다.
- Garbage Collection을 지원한다.
- 전처리기를 사용하지 않아 컴파일 속도가 빠르다
- 정적 링크를 사용하여 생성된 바이너리 파일을 생성한다. 그러하여 해당 바이너리를 실행하는데 필요한 라이브러니나 의존성을 신경 쓸 필요 없다.
- 유니코드를 지원한다.
Go 언어 단점
- OOP를 직접 지원하지 않으나, Composition으로 상속을 어느 정도 흉내 낼 수 있다.
- C개발자 입장에서는 Go는 절대로 C를 대체할 수 없다.
전처리기란?
전처리기란 개발자가 입력한 데이터를 처리해서 그 결과를 다른 프로그램의 입력으로 사용하는 프로그램이다. 프로그래밍 언어에서의 전처리기 입력은 소스 코드이며, 전처리기는 이 코드를 가공하여 프로그래밍 언어의 컴파일러에 입력 값으로 전달한다.
전처리기의 단점은 코드의 내용이나 언어의 문법에 대해 전혀 모른다는 것이다.
godoc 유틸리티
Go 배포판에서는 프로그래머의 일을 줄여주는 도구가 방대하게 제공된다. godoc은 인터넷에 연결되어 있지 않아도 Go 함수나 패키지에 대한 문서를 쉽게 볼 수 있다.
Printf() 함수에 대해 알고 싶을 때
godoc fmt Printf
-http 매개변수를 지정하여 브라우저 상에서도 확인할 수 있다.
godoc -http=:8001
Go 코드 컴파일하기
Go 언어는 소스 파일의 이름을 신경 쓰지 않는다. 패키지 이름이 main이고 그 안에 main()함수만 정의돼 있으면 하나의 독립 실행 프로그램으로 취급한다. 따라서 한 프로젝트 안에서 여러 파일에 main() 함수를 정의 할 수 없다.
package main
import "fmt"
func main() {
fmt.Println("This is a sample Go program!")
}
go build aSourceFile.go
aSourceFile이란 실행 파일이 생성될 것이다.
Go 코드 실행하기
영구정인 실행 파일 형태로 만들지 않고도 Go 코드를 실행하는 방법이 있다. 이렇게 하면 중간 코드만 생성했다가 나중에 자동적으로 삭제된다.
go run aSourceFile.go
go run으로 실행하더라도 Go 컴파일러는 실행 파일을 생성한다. 이 파일을 내부적으로 실행하여 프로그램이 끝나면 자동으로 삭제되기 때문에 마치 실행 파일을 전혀 사용하지 않는 것처럼 보일 뿐이다.
Go 언어의 두 가지 규칙
사용하지 않을 Go 패키지는 임포트 하지 말 것
Go 언어는 패키지 사용에 대해 엄격한 규칙을 적용한다.
package main import ( "fmt" "os" ) func main() { fmt.Println("This is a sample Go program!") }
이 파일을 실행하면 컴파일러는 "imported and not used : 'os'"라는 에러를 뱉을 것이다.
package main import ( "fmt" _ "os" ) func main() { fmt.Println("This is a sample Go program!") }
위와 같이 패키지 명 앞에 언더스코어를 붙이면, 컴파일 에러가 발생하지 않는다.
중괄호 작성 스타일을 따를 것
package main import "fmt" func main() { fmt.Println("This is a sample Go program!") }
위 코드는 아무런 문제가 없어 보이지만, 실행해보면 문법 에러 메시지가 발생한다.
공식 문서의 설명에 따르면, Go 언어는 세미콜론으로 문장의 끝을 표현하며, 컴파일러는 필요하다는 판단되는 지점에 세미콜론을 집어넣는다. 따라서 {로 문장을 시작하면 Go 컴파일러는 그 이전 문장의 끝에 세미콜론을 넣어 에러 메시지가 발생한다.
Go 패키지 다운로드하기
프로그래밍을 하다 보면 외부 패키지를 사용하고 싶을 때가 있다.
package main
import (
"fmt"
"github.com/mactsouk/go/simpleGitHub"
)
func main() {
fmt.Println(simpleGitHub.AddTwo(5,6))
}
여기에 사용되는 외부 패키지 이름은 simpleGitHub로서 https://github.com/mactsouk/go에 저장돼어 있다.
실행하면 에러 메시지를 볼 수 있는데, 패키지가 머신에 설치되어 있지 않기 때문이다.
go get -v github.com/mactsouk/go/simpleGitHub
커맨드라인에 입력 하면 다운로드 파일이 있는 것을 확인 할 수 있다.
유닉스 stdin, stdout, stderr
모든 유닉스 OS는 OS에서 실행되는 프로세스를 위해 항상 세 가지 파일을 열어둔다. 유닉스는 File Descriptor를 사용하는데, 열린 파일에 접근하기 위한 내부 표현 수단이다.
유닉스 시스템은 /dev/stdin, /dev/stdout, /dev/stderr라는 세가지 특수한 표준 파일명을 사용하는데, 각 파일에 대한 파일 디스크립터는 0,1,2이다. 이 세가지를 표준 입력, 표준 출력, 표준 에러라고 부른다.
Go 코드에서 표준 입력은 os.Stdin, 표준 출력은 os.Stdout, 에러는 os.Stderr로 접근이 가능하다.
화면에 출력하기
Go 언어에서 화면에 출력하기 위한 가장 간단한 방법은 fmt.Println()과 fmt.Printf() 함수를 사용하는 것이다. Printf()는 화면에 출력할 대상마다 서식 지정자를 지정해야하는데 Go에서는 Verb라 부른다.
표준 출력 사용하기
package main
import (
"fmt"
"io"
"os"
)
func main() {
myString := ""
arguments := os.Args
if len(arguments) == 1 {
myString = "Please give me one arg!"
} else {
myString = arguments[1]
}
io.WriteString(os.Stdout, myString)
io.WriteString(os.Stdout, "\n")
}
fmt 패키지 대신 io 패키지를 사용한다. os 패키지는 프로그램에서 커맨드 라인 인수를 읽고 os.Stdout에 접근하기 위해 사용된다.
myString 변수는 화면에 출력할 텍트를 담는다.
io.WriteString 함수는 fmt.Print()와 똑같은 방식으로 작동하지만 두 개의 매개변수만 받는다는 점이 다르다. 첫번째 매개변수는 쓰려는 파일을 지정하고 두 번째 매개변수는 string 타입 변수이다.
- 엄밀히 말해서 첫번째 매개변수는 io.Write 타입이어야한다.
사용자로부터 입력 받기
사용자로부터 입력을 받는 방법은 크게 3가지 있다
- 프로그램의 커맨드라인 인수를 읽는다.
- 사용자에게 입력 값을 물어본다
- 외부 파일을 읽는다
:= 와 =
사용자의 입력을 받는 것에 대해 설명하기전 :=, =가 어떨 때 사용되고 둘의 차이점을 알아본다.
:=의 공식 명칙은 짧은 할당문이다. := 대신 묵시적 타입을 사용하는 var 키워드를 사용할 수 있다.
- var 키워드는 보통 전역 변수를 선언하거나, 초기값을 지정하지 않고 변수를 선언할 때 사용된다. 외부에 존재하는 모든 문장은 반드시 func나 var과 같은 키워드로 시작해야 하기 때문이다
이미 선언된 변수에 :=를 적용하면 컴파일 에러가 발생한다.
표준 입력으로 부터 받기
package main
import (
"bufio"
"fmt"
"os"
)
bufio 패키지는 파일 입력 및 출력을 위해 추가하였다.
os 패키지에 대해 공식 문서를 읽어보면 OS 연산을 수행하는 함수를 제공한다고 한다. 이러한 함수로 파일을 생성, 삭제하고 파일 및 디렉토리의 이름을 바꾸는 것 뿐만 아니라, 유닉스 접근 권한을 비롯한 파일과 디렉토리에 대한 다양한 속성을 확인할 수 있다.
func main() {
var f *os.File
f = os.Stdin
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Pritln(">", scanner.Text())
}
}
bufio.NewScanner()의 매개변수로 표준 입력을 넘겨 호출하는 것을 볼 수 있다. 이 함수는 bufio.Scanner 변수를 리턴하는데, 이 값은 다시 Scan() 함수로 os.Stdin으로 부터 한 줄 씩 읽는데 사용된다.
커맨드라인 인수 다루기
package main
import (
"fmt"
"os"
"strconv"
)
커맨드라인 인수를 가져오려면 os 패키지를 이용 한다는 점이 중요하다. 또한 strconv 패키지도 필요한데, 스트링 타입으로 입력된 커맨드 라인 인수를 산술 데이터 타입으로 변환해야 하기 때문이다.
func main() {
if len(os.Args) == 1 {
fmt.Println("Please Give one or more floats.")
os.Exit(1)
}
arguments := os.Args
min, _ := strconv.ParseFloat(arguments[1], 64)
max, _ := strconv.ParseFloat(arguments[1], 64)
먼저 os.Args의 길이를 검사하여 커맨드라인 인수가 들어왔는지 확인한다. 이 때 os.Args는 string 값을 가진 Go 슬라이스입니다. 슬라이스의 첫 번째 원소는 항상 실행 가능한 프로그램의 이름이다. 따라서 커맨드 라인 인수는 인덱스 1부터 접근해서 가져온다.
이 때 min과 max옆에 있는 _는 빈 식별자라 부른다. Go 언어에서 어떤 값을 무시할 때 이렇게 표현한다.
에러 출력
데이터를 유닉스 표준 에러로 보내는 방법에 대해 소개한다. 유닉스에서는 실제 값과 에러 출력을 방법으로 구분한다.
io.WriteString(os.Stdout, myString)
위 표준 출력 예제에서 os.Stdout가 있는 파일 디스크립터 지정하는 자리에 os.Stderr로 바꾸면 표준 에러에 쓰여진다. 결과만 보면 표준 출력과 표준 에러를 구분할 수 없다. 때로는 이 점이 굉장히 유용하기도 한다. bash 쉘을 사용한다면 나름 유용하게 쓸수 있다.
$ go run stdErr.go 2>/tmp/stdError
This is standard output
$ cat /tmp/stdError
Please giv e one argument!
stderr를 무시하고 싶으면 /dev/null로 리다이렉션 하면 된다.
로그 파일 작성하기
log 패키지를 사용하면 유닉스 머신의 시스템 로그 서비스로 로그 메시지를 보낼 수 있다. 패키지의 일부로 제공하는 syslog라는 go 패키지를 사용하면 go 프로그램에 적용할 logging level과 logging facility를 지정할 수 있다.
일반적으로 유닉스 os에서는 시스템 로그 파일은 대부분 /var/log 디렉토리에서 볼 수 있다. Apache나 Nginx 같은 유명한 서비스는 로그 파일을 다른 곳에 지정하기도 한다.
일반적으로는 로그 파일을 쓰는 것이 바람직한 습관으로 알려저 있는데, 그 이유는
- 파일에 저장하기 때문에 결과가 사라지지 앟는다.
- grep, awk, sed같은 유닉스 커맨드로 로그 파일을 검색하거나 가공하기 좋다.
log 패키지는 출력을 유닉스의 syslog 서버로 보내는 기능을 다양한 함수로 제공한다.
로그 수준
로그 수준이란 로그 항목의 심각한 정도를 나타내는 값이다. debug, info, warning, 등이 있다
로그 종류
로그 정보가 속한 범주를 의미한다. 로그 종류의 값으로는 auth, user, syslog 등이 있다. 이런 값은 /etc/syslog.conf, /etc/rsyslog.conf에 정의돼 있거나, 사용하는 유닉스 머신에서 시스템 로그를 수행하는 서버 프로세스 내에 별도로 지정한 파일에 정의도기도 한다. 로그 종류를 정의하지 않으면, 보낸 로그 메시지가 무시되어 사라지게 된다.
로그 서버
유닉스 머신마다 로그 데이터를 받아서 로그 파일에 기록하는 별도의 서버 프로세스가 존재한다. 로그 서버는 다양한데, 그 중 syslogd와 rsyslogd가 많이 사용된다.
rsyslogd에 대한 설정은 흔히 rsyslog.conf라는 이름의 파일에 정의하며 /etc 밑에 있다.
로그 파일로 정보를 보내는 Go 프로그램
package main
import (
"fmt"
"log"
"log/syslog"
"os"
"path/filepath"
)
func main() {
programName := filepath.Base(os.Args[0])
sysLog, err := syslog.New(syslog.LOG_INFO|syslog.LOG_LOCAL7, programName)
syslog.New() 함수의 첫 번째 매개변수로 우선순위를 지정한다. 이 값의 로그 종류와 로그 수준을 한꺼번에 조합해서 표현한다. LOG_NOTICE | LOG_MAIL로 지정하면 로그 종류는 mail이고 로그 수준은 notice인 메시지를 보내게 된다.
위 코드는 로그 종류를 log7로 지정하고 로그 수준은 info로 설정한다. syslog.New()함수의 두 번째 매개변수는 메시지를 보내는 프로세스의 이름을 지정하며, 로그에 이 값이 표시된다. 일반적으로 실제 수행되는 실행 파일의 이름을 지정하는 것이 바람직하다. 그래야 ㅏ눚에 로그 파일을 보고 필요한 정보를 쉽게 찾을 수 있다.
if err != nil {
log.Fatal(err)
} else {
log.SetOutput(sysLog)
}
log.Println("LOG_INFO + LOG_LOCAL7 : Logging in GO!")
syslog.New()를 호출한 뒤에, 리턴하는 에러ㅏ.변수를 보고 모든 것이 순조롭게 진행되는지 반드시 확인할 필요가 있다. 문제가 없다면 log.SetOutput 함수를 호출한다. 이 함수는 디폴트 로거가 출력할 지점을 설정한다. 위 코드에서는 syslog로 설정한다. 그런 다음 log.Println()을 호출해 정보를 로그 서버로 보낸다
sysLog, err = syslog.New(syslog.LOG_MAIL, "Some Program!")
if err != nil {
log.Fatal(err)
} else {
log.SetOutput(sysLog)
}
log.Println("LOG_MAIL : Logging in Go!")
fmt.Println("Will you see this?)
}
유닉스 머신에 있는 로그 서버에서 모든 로그 기능을 사용하도록 설정하지 않으면, 무시될수도 있음을 알자
log.Fatal()
이 함수는 정말 나쁜 일이 발생해서 상황을 알려주자마자 프로그램을 종료하고 싶을 때 사용한다
package main
import (
"fmt"
"log"
"log/syslog"
)
func main() {
sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some Program")
if err != {
log.Fatal(err)
} else {
log.Setutput(syslog)
}
log.Fatal(syslog)
fmt.Println("You cant' c me")
}
이 코드는 실행하면 마지막 줄을 보지 못하고 꺼질 것이다.
하지만 syslog.New()를 호출 할 때 지정한 매개변수로 인해 mail에 관련된 로그 파일에 해당 항목이 추가된다.
log.Panic()
때로는 프로그램이 다시 실행될 수 없을 정도로 오류가 발생하는 순간, 이에 관련된 정보를 최대한 알고 싶을 때가 있다. 이렇게 까다로운 상황에서 사용할 만한 함수로 log.Panic()이 있다.
package main
import (
"fmt"
"log"
"log/syslog"
)
func main() {
sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some Program")
if err != {
log.Fatal(err)
} else {
log.Setutput(syslog)
}
log.Panic(syslog)
fmt.Println("You cant' c me")
}
log.Panic()으로 출력한 결과를 보면 저수준의 정보도 포함한다.
Go에서 에러 처리하기
Go 언어는 에러 메시지를 너무 좋아한 나머지, error 라는 전용 데이터 타입까지 갖고 있다. 그래서 Go에서 제공하는 메시지만으로 부족하다 싶으면 자신이 직접 에러 메시지를 정의하기 쉽다.
에러가 발생하는 것과 에러를 어떻게 처리할 것인지는 전혀 다른 문제이다. 쉽게 말해, 모든 에러를 똑같이 취급할 수 없다. 어떤 에러는 발생 즉시 프로그램을 멈춰야 하고, 다른 에러는 경고 메시지 정도만 출력할 수 있다.
error 데이터 타입
Go 애플리케이션을 작성하다보면 처리해야 할 에러 타입을 새로 정의해야 할 필요가 있다.
새로운 error변수를 생성하려면 Go 패키지 errors에서 New()함수를 호출해야한다.
package main
import (
"errors"
"fmt"
)
func returnError(a, b int) error {
if a == b {
err := erros.New("Error in returnError() function!")
return err
} else {
return nil
}
}
errors.New()함수는 스트링 타입의 값을 매개변수로 받는다.
func main() {
err := returnError(1, 2)
if err == nil {
fmt.Println("returnError() ended normally!")
} else {
fmt.Println(err)
}
err = returnError(10, 10)
if err == nil {
fmt.Println("returnError() ended normally!")
} else {
fmt.Println(err)
}
if err.Error() == "Error in returnError() function!" {
fmt.Println("!!")
}
}
대부분의 경우 error 변수의 값이 nil인지 여부를 확인한다.
err.Error() 메소드를 사용하면 error 변수를 string 타입의 변수로 변환할 수 있다.
에러 처리하기
에러 처리는 Go에서 중요한 기능이다. Go 언어를 함수로 작성할 때 에러 메시지나 nil을 리턴하는 방식으로 그 함수에서 에러가 발생했는지 알려준다.
if err != nil {
fmt.Println(err)
os.Exit(10)
}
- 에러 처리와 에러 출력을 헷갈리면 안된다. 에러 처리는 go 언어에서 에러 상황에 대처하는 기능인 반면, 에러 출력은 단순히 표준 에러 파일 디스크립터로 데이터를 쓰는 것이다
만약 정말 좋은 상황이 발생해서 프로그램을 즉시 종료하고 싶다면 다음과 같이 이렇게 작성한다
if err 1= nil {
panic(err)
os.Exit(10)
}
panic 함수는 프로그램을 멈추고 패닉 상태에 빠지게 한다. panic함수를 너무 많이 사용하고 있다면, 코그들 다시 설계할 필요가 있다. Go 언어에서는 recove라는 함수도 제공한데, 좋지 않은 상황이 발생했을 때 빠져나오는데 유용하다.
이제 자신이 직접 에러 메시지를 정의하는 방법에 대해 알아본다.
package main
import (
"errors"
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) == 1 {
fmt.Println("Please give me one or more floats")
os.Exit(1)
}
arguments := os.Args
var err error = errors.New("An error")
k := 1
var n float64
여기서 err란 이름으로 새로운 error변수를 정의하고 원하는 값으로 초기화 하였다.
if err != nil {
if k >= len(arguments) {
fmt.Println("None of the arguments is float!")
return
}
n, err = strconv.ParseFloat(arguments[k], 64)
k++
}
min, max := n, n
for i:=2; i < len(arguments); i++ {
n, err := strconv.ParseFloat(arguments[i], 64)
if err == nil {
if n < min {
min = n
}
}
if n > max {
max = n
}
}
fmt.Println("Min : ", min)
fmt.Println("Max : ", max)