Go Lang 시작하기
What is Go Lang?
Go언어는 구글에서 만든 프로그래밍 언어로 시스템 프로그래밍에 적합하도록 설계되었으며, C와 구문이 비슷하다.
하지만 C와 C++의 복잡한 요소를 최대한 줄이고 간결하게 만들어졌다.
Quick Start
Go 공식 홈페이지에서 설치 파일을 다운받도록 하자.
Downloads - The Go Programming Language
Downloads After downloading a binary release suitable for your system, please follow the installation instructions. If you are building from source, follow the source installation instructions. See the release history for more information about Go releases
go.dev
자신의 OS에 맞게끔 설치하면 된다.
다운로드가 완료되면 받은 Go 설치파일을 실행한다.
Next 버튼을 눌러준다.
동의하기를 눌러주고 Next를 눌러준다.
여기서 Go를 설치할 폴더 경로를 설정해줘야한다.
만약 설치 경로를 바꿔야 한다면 바꾼 후 Next를 눌러주자.
Install 버튼을 눌러 설치를 진행한다.
설치 완료!
이제 환경변수를 편집해주자.
윈도우에서 시스템 환경변수 -> 사용자 변수로 들어가면 GOPATH가 설정되어있을 것이다.
이 GOPATH가 실제 Go 프로젝트를 진행할 프로젝트 경로를 말하는 것인데 이 경로를 본인이 프로젝트 폴더를 놓고 싶은 곳으로 바꿔주자.
환경변수 편집이 끝났다면 GOPATH로 지정해준 폴더 하위로 들어가자.
아무것도 없는 폴더 안에 각각 bin,pkg,src 라는 이름의 폴더를 만들어주자.
각각의 폴더의 역할은 다음과 같다.
폴더명 | 역할 |
bin | 소스 코드 컴파일 후 운영체제 별로 실행 가능한 바이너리 파일이 저장됨 |
pkg | 프로젝트에 필요한 패키지가 컴파일되어 라이브러리 파일이 저장되는 곳 |
src | 직접 작성한 소스 코드 및 오픈 소스 코드를 저장하는 곳 |
이제 VS Code에 Go를 설치해주자.
Go install 클릭
이제 src 폴더 안에 소스 파일을 생성하고 Hello World를 찍어보자.
만약 Couldn’t start dlv dap: Error:timed out while waiting for DAP server to start 라는 에러가 발생 시
해결법을 참고하길 바란다.
Feature of Go
1. Keyword of Go
golang에서 지원하는 키워드는 25개이다.
while 문은 없고 for 문만으로 반복을 표현한다.
c언어와는 다르게 switch 문 case에 조건식을 사용할 수 있다.
2. Data types
3. Variables & Constants
-Variables
var <변수명> <자료형> = <초기값>
var a int
var f float32 = 11.
var i,j, k int = 1, 2, 3
var d = 5
변수는 생성시 Zero Value(0, false, "")를 default로 가진다.
타입을 생략할 수 있긴 하다. 타입 추론이 가능하기 때문인데 이는 Go가 initial value를 통해 타입을 추론해서 타입을 지정하기 때문이다. 따라서 타입을 생략할 때는 initialization을 해주어야 한다.
Short Assignment Statement( := ) 를사용할 수도 있다. 하지만 함수 안에서만 사용 가능하다.
-Constants
const 키워드를 사용하여 상수를 선언한다. 맨 앞에 var 대신 const를 붙이면 된다.
타입을 지정하지 않고 문맥에 따라 타입을 가지게 된다.
여러개를 괄호 ( ( ) )로 묶어 한번에 지정할 수 있다.
const <변수명> = <값>
const (
<변수명> = <값>
)
const (
<변수명> = iota //0부터 auto increase
<변수명2>
)
iota라는 identifier를 사용하면 상수 값을 순차적으로 0부터 부여할 수 있다.
4. String
Back Quote(``)와 Double Quote("")로 선언할 수 있다. (single quote('')는 룬(rune, 유니코드를 표현하는 타입) 타입에 사용한다. )
Back Quote로 쓰인 문자열은 Raw String Literal로, 문자열이 별도로 해석되지 않는다. 예를 들어 '\n'은 new line으로 해석되지 않고 문자 그대로 사용된다.
Double Quote가 흔히들 알고 있는 문자열인 Iterpreted String Literal이다.
String은 immutable type으로 한번 생성되면 수정할 수가 없다.
5. Type Conversion
Go에서는 암묵적인 type conversion이 이뤄지지 않고 항상 명시적으로 지정해주어야 한다.
<자료형>(<변수명>)
var i int = 100
var u uint = uint(i)
6. Operator
-arithmetic operator ( 산술 연산자 )
산술연산자는 사칙연산자(+,-,*,/,%)와 증감연산자(++,--)를 사용한다.
c = (a + b) / 5;
i++;
-comparison operator ( 관계 연산자 )
관계연산자는 서로의 크기를 비교하거나 동일함을 체크하는데 사용된다.
a == b
a != c
a >= b
-logical operator ( 논리 연산자 )
논리연산자는 AND, OR, NOT을 표현하는데 사용된다.
A && B
A || !(C && B)
-bitwise operator ( 비트 연산자 )
비트연산자는 비트단위 연산을 위해 사용되며 binary AND, OR, XOR과 binary shift 연산자가 있다.
c = (a & b) << 5
-assignment operator ( 대입 연산자 )
대입연산자는 값을 대입하는 = 연산자 외에 사칙연산, 비트연산을 축약한 +=, &=, <<=와 같은 연산자들도 있다.
a = 100
a *= 10
a >>= 2
a |= 1
-pointer operator ( 포인터 연산자 )
포인터 연산자는 C++과 같이 & 혹은 *을 사용하여 해당 변수의 주소를 얻어내거나 이를 반대로 Dereference할 때 사용한다. Go는 비록 포인터연산자를 제공하지만 포인터 산술 기능은 제공하지 않는다.
var k int = 10
var p = &k //k의 주소를 할당
println(*p) //p가 가리키는 주소에 있는 실제 내용을 출력
7. if / switch 조건문
-if / else 문
if i == 1 { 실행할 코드 }
else if i == 2 { 실행할 코드 }
else { 실행할 코드 }
if 다음에는 반드시 Boolean 식으로 표현한다. (0,1 등 사용 못함)
()는 쓰지 않지만 {}는 무조건 사용해야 한다.
반드시 조건 블럭 시작 브레이스({)를 if문과 같은 라인에 두어야 한다.
또한 조건식에 코드 삽입이 가능하다.
if value := i*2; value < max {
println(value)
}
value++ // Error, 변수는 block scope
-switch / case 문
switch {변수 또는 표현식} {
case {value}:
default:
}
break문이 없어도 되며 표현식 사용이 가능하다.
// 0 0 0 1
// 8 4 2 1
category := 1
switch x := category << 2; x {
case 1:
case 4:
println("hello")
}
switch 다음에 변수나 표현식이 없어도 되며 fallthrough 키워드를 사용하지 않는다면 case가 중첩되지 않는다.
case에는 조건식과 자료형 사용이 가능하다.
8. loop 문
-for문
for 정의 ; 조건문 ; 증감 { 실행할 코드 }
for 조건문 { 실행할 코드 }
for { 무한루프 }
for 인덱스, 값 := range 컬렉션 { 실행할 코드 }
break, continue, goto 존재
특이하게 break를 goto처럼 사용가능
9. Function
func 함수명 (파라미터) {실행할 코드}
func 뒤에 함수명을 적고 괄호 ( ) 안에 그 함수에 전달하는 파라미터들을 적게 된다. 함수 파라미터는 0개 이상 사용할 수 있으며 각 파라미터는 파라미터 명 뒤에 int, string 등의 파라미터 타입을 적어서 정의한다. 함수의 리턴 타입은 파라미터 괄호 ( ) 뒤에 적게 된다. 함수는 패키지 안에 정의되며 호출되는 함수가 호출하는 함수의 반드시 앞에 위치해야 할 필요는 없다.
Go에서 파라미터를 전달하는 방식은 크게 Pass By Value와 Pass By Reference로 나뉜다.
-Pass By Value
인자의 값을 복사해서 전달
-Pass By Reference
인자의 값이 아닌 인자의 값이 저장된 메모리 주소를 전달
함수에 다양한 숫자의 파라미터를 전달하고자 할 때 가변 파라미터를 나타내는 ... (3개의 마침표)를 사용한다.
즉 문자열 가변 파라미터를 나타내긴 위해선 ...string 과 같이 표현한다. 가변 파라미터를 갖는 함수를 호출할 때는 n개의 동일 타입 파라미터를 전달할 수 있다.(이처럼 가변 파라미터를 받아들이는 함수는 Variadic Function이라고 부른다.)
Go에서 함수는 리턴값이 없을 수도, 리턴값이 하나일수도, 또는 리턴값이 복수 개일수도 잇다.
또한 Named Return Parameter라는 기능을 제공하는데, 이는 리턴되는 값들을 함수에 정의된 리턴 파라미터들에 할당할 수 있는 기능이다.
함수에서 리턴값이 있는 경우는 func 문의 마지막(파라미터 괄호 다음)에 리턴값의 타입을 정의해준다. 그리고 값을 리턴하기 위해 함수 내에서 return 키워드를 사용한다.
복수 개의 값을 리턴하기 위해서는 해당 리턴 타입들을 괄호 ( ) 안에 적어준다. 예로 처음 리턴값이 int이고 두번째 리턴값으 string인 경우 (int, string)과 같이 적어준다.
Go에서 Named Return Parameter들에 리턴값들을 할당하여 리턴할 수 있는데 이는 리턴되는 값들이 여러 개일 때, 코드 가독성을 높여준다.아래 예제에서 func 라인의 마지막 리턴타입 정의 부분을 보면, (int, int) 가 아니라 (count int, total int) 처럼 정의되어 있음을 볼 수 있다. 즉, 리턴 파라미터명과 그 타입을 함께 정의한 것이다. 그리고 함수 내에서는 이 count, total에 결과값을 직접 할당하고 있음을 볼 수 있다. 또한 마지막에 return 문이 있는 것을 볼 수 있는데, 실제 return 문에는 아무 값들을 리턴하지 않지만, 그래도 리턴되는 값이 있을 경우에는 빈 return 문을 반드시 써 주어야 한다 (이를 생략하면 에러 발생).
func sum(nums ...int) (count int, total int) {
for _, n := range nums {
total += n
}
count = len(nums)
return
}
10. Anonymous function
함수명을 갖지 않는 함수는 익명함수(Anonymous Function)이라 부른다. 일반적으로 익명함수는 그 함수 전체를 변수에 할당하거나 다른 함수의 파라미터에 직접 정의되어 사용되곤 한다.
변수 := func ( 파라미터 ) 반환 자료형 { 실행할 코드 }
11. First-class Function
Go에 함수는 일급함수로서 Go의 기본 타입과 동일하게 취급되며, 따라서 다른 함수의 파라미터로 전달하거나 다른 함수의 리턴값으로 사용될 수 있다.
12. Function as type
type 원형 함수명 func ( 파라미터 ) 반환 자료형 // 선언하고
func 함수명 (f 원형 함수명, 파라미터) 반환 자료형 { 실행할 코드 } // 정의한다.
함수의 원형을 하나의 type으로 만들 수 있다.
이렇게 함수의 원형을 정의하고 함수를 타 메서드에 전달하고 리턴받는 기능을 타 언어에선 Delegate(델리게이트, 위임)라고 부른다.
13. Closure
함수는 Closure로서 사용될 수도 있다. Closure는 함수 바깥에 있는 변수를 참조하는 함수값(function value)를 일컫는데 이때의 함수는 바깥의 변수를 마치 함수 안으로 끌어들인 듯이 그 변수를 읽거나 쓸 수 있게 된다. 즉 리턴된 함수는 처음에 호출한 함수의 변수들을 사용할 수 있다.
func 함수명 () func() int {
i:=0
return func() int {
i ++
return i
}
}
14. Array
var 변수명 [개수] 자료형
var 변수명 […] 자료형 {1,2,3} // 배열크기 자동처리
다차원 배열 가능
배열의 선언은 "var 변수명[배열크기] 데이터타입"과 같이 하는데 Go에서 배열의 배열크기는 Type을 구성하는 한 요소이다. 즉 [3]int와 [5]int는 서로 다른 타입으로 인식된다. 배열을 정의할 때 초기값을 설저할 수도 있다. 초기값은 "[배열크기] 데이터타입" 뒤에 { } 괄호를 두고 초기값을 순서대로 적으면 된다.
Go 언어는 다차원 배열을 지원한다. 다차원 배열은 배열크기 부분을 복수 개로 설정하여 선언한다.
15. Slice
var 변수명 []자료형 // 크기 입력 안할 시 slice 변수로 선언됨
변수명 := make([]자료형, Length, Capacity)
변수명[n:m] // n부터 m까지 잘라낼 수 있다. [:]은 전체
변수명 = append(변수명, 추가 요소) // 자유롭게 추가 가능
Go 배열은 고정된 배열 크기 안에 동일한 타입의 데이터를 연속적으로 저장하지만 배열의 크기를 동적으로 증가시키거나 부분 배열을 발췌하는 등의 기능을 가지고 있지 않다. Go slice는 내부적으로 배열에 기초하여 만들어졌지만 배열의 제약점을 넘어 유용한 기능을 제공한다. 슬라이스는 고정된 크기를 미리 지정하지 않을 수 있고 크기를 동적으로 변경할 수도 있으며 부분 배열을 발췌할 수도 있다.
16. Map
변수명 := map[자료형]자료형
Map은 키(key)에 대응하는 값(value)을 신속히 찾는 해시테이블(Hash table)을 구현한 자료구조이다.
Go는 Map 타입을 내장하고 있는데, "map[Key타입]Value타입" 과 같이 선언할 수 있다.
map에 Key가 존재하지 않을 경우 nil( reference 자료형 ) 또는 Zero value를 반환한다.
정수를 키로하고 문자열을 값으로 하는 맵 변수 idMap을 선언하기 위해서는 다음과 같이 할 수 있다.
var idMap map[int]string
key 존재 여부 확인방법
value, exists := tickets["twice"]
if !exists {
println("No twice ticket")
}
Map iterate
tickets := map[string]int{
"twice": 0,
"exhibition": 2
}
for key, value := range tickets{
fmt.Println(key, value)
}
17. Package
Go는 패키지(Package)를 통해 코드의 모듈화, 코드의 재사용 기능을 제공한다. Go는 패키지를 사용해서 작은 단위의 컴포넌트를 작성하고, 이러한 작은 패키지들을 활용해서 프로그램을 작성할 것을 권장한다.
가장 작은 단위의 Component
기본 패키지: $GOROOT/pkg
메인 패키지: main - 컴파일러가 실행 프로그램으로 인식하는 Entry point
그 외 패키지: GOROOT또는 GOPATH 환경변수 기준으로 검색
18. Struct
Custom Data Type
Field의 집합체이자 컨테이너
다른 언어와 달리 메서드를 갖지 않음
일반적인 객체지향 언어와 다른 방식으로 지원
메서드는 별도로 분리하여 정의
package main
import "fmt"
type Person struct{
name string
age int
}
func main(){
p := Person{}
p.name = "Name"
p.age = 100
fmt.Println(p)
}
객체 생성 방법
var p1 Person // 변수 선언
p1 = Person{"name", 100} // 특이하게 중괄호 사용
p2 := Person{name:"name", age: 100} // dictionary 처럼 사용 가능
p := new(Person) // 객체의 pointer를 반환하는 new 함수
p.name = "Name" // pointer도 .(dot) 연산자로 접근 가능
기본적으로 Go의 struct는 mutable으로 메모리에서 직접 변경
그러나 func의 param으로 넘길 경우 주소가 아닌 값을 전달(Pass by value)
생성자 함수
생성자가 없으므로 직접 구현해주어야 한다.
package main
type Dict struct{
data map[int]string
}
func newDict() *Dict{
d := Dict{}
d.data = map[int]string{}
return &d //포인터 반환
}
func main(){
dic := newDict() // 생성자 호출
dic.data[1] = "A"
}
19. method
Go의 객체지향 프로그래밍을 위한 기법
필드만 정의된 struct에 함수를 정의하여 구현
메서드는 특별한 형태의 func 함수이다. 메서드는 함수 정의에서 func 키워드와 함수명 사이에 "그 함수가 어떤 struct를 위한 메서드인지"를 표시하게 된다. 흔히 receiver로 불리우는 이 부분은 메서드가 속한 struct 타입과 struct 변수명을 지정하는데, struct 변수명은 함수 내에서 마치 입력 파라미터처럼 사용된다. 예를 들어, 아래 예제는 Rect라는 struct를 정의하고 area() 라는 메서드를 정의하고 있다. func와 area() 사이에 Rect 타입의 r 이 정의되고 이를 함수 본문에서 사용하고 있다. 메서드가 선언된 이후에는 Rect 구조체의 객체는 rect.area() 문장처럼 area() 메소드를 struct 객체로부터 직접 호출할 수 있다.
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)
}
Value receiver vs Pointer receiver
-Value receiver는 struct의 데이터를 복사(copy)하여 전달하며, Pointer receiver는 struct의 포인터만을 전달한다. Value receiver의 경우 만약 메서드 내에서 그 struct의 필드값이 변경되더라도 호출자의 데이터는 변경되지 않는 반면, Pointer reciever는 메서드 내의 필드값 변경이 그대로 호출자에서 반영된다.
위의 Reat.area() 메서드는 Value receiver를 표현한 것으로 만약 area() 메서드 내에서 width나 height가 변경되더라고 main() 함수의 rect 구조체의 필드값에는 변화가 없다. 하지만 아래와 같이 이를 Pointer receiver로 변경한다면, 메서드 내 r.width++ 필드 변경분이 main() 함수에서도 반영되기 때문에 출력값이 11, 220이 된다.
// 포인터 Receiver
func (r *Rect) area2() int {
r.width++
return r.width * r.height
}
func main() {
rect := Rect{10, 20}
area := rect.area2() //메서드 호출
println(rect.width, area) // 11 220 출력
}
20. interface
Struct는 field의 집합체
Interface는 method의 집합체
다른 언어의 최상위 객체와 동일
type 인터페이스명 interface{
함수명() 반환형
}
인터페이스는 struct와 마찬가지로 type문을 사용하여 정의한다.
인터페이스의 메소드를 다른 구조체에서 구현할 경우 Interface를 이용하여 다형성 구현이 가능
Interface type
func 함수명(v interface{}) (n int, err error);
interface는 어떠한 값도 담을 수 있는 컨테이너 역할을 한다.
Type assertion
interface의 자료형을 강제하는 기법
var a interface{} = 1
i := a // a와 i는 동적 타입, 값은 1
j := a.(int) // j는 int형, 값은 1
println(i) // 주소값 출력
println(j) // 1 출력
21. Error
Go에서 Error는 내장 타입, 제일 빈번하게 사용
type error interface{
Error() string
}
22. defer, panic, recover()
return 직전에 수행되는 가장 마지막 호출 함수 키워드 defer
에러 발생 시 현재 로직을 즉시 중단 하고 defer를 모두 실행시킨 후 강제로 종료 하는 키워드 panic
panic 상태에서 정상상태로 돌린 후 다음 로직을 수행하게 하는 함수 recover()
23. routine
Go 런타임이 관리하는 경량 쓰레드
호출 방법은 함수 앞에 go 키워드만 붙이면 끝
Goroutine
OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent 처리를 구현하기 위하여 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다. Go 런타임 상에서 관리되는 작업단위인 여러 goroutine들은 종종 하나의 OS 쓰레드 1개로도 실행되곤 한다. 즉, Go루틴들은 OS 쓰레드와 1 대 1로 대응되지 않고, Multiplexing으로 훨씬 적은 OS 쓰레드를 사용한다.
메모리 측면에서도 OS 쓰레드가 1 메가바이트의 스택을 갖는 반면, goroutine은 이보다 훨씬 작은 몇 킬로바이트의 스택을 갖는다(필요시 동적으로 증가). Go 런타임은 Go루틴을 관리하면서 Go 채널을 통해 Go루틴 간의 통신을 쉽게 할 수 있도록 하였다.
Thread는 16개의 범용 레지스터, Program Counter, Stack Pointer, Segment Register, 16 XMM registers, FP coprocessor state, 16 AVX registers, all MSRs etc를 스위칭
Goroutine은 Program Counter, Stack Pointer, DX register
// helloWorld.go
package main
import (
"fmt"
"time"
)
func say(s string){
println(s)
}
func main(){
say("hell")
go say("o")
go say("wo")
go say("rld")
time.Sleep(time.Second * 3)
}
$ go run helloWorld.go
hell
wo
rld
o
실행 시점마다 다르게 출력된다.
익명함수 Goroutine
go func(s string){
println(s)
}("hello world")
Goroutine sync
var wait sync.WaitGroup
wait.Add(개수)
go func(s string){
defer wait.Done()
println(s)
}("hello world")
wait.Wait()
sync.WaitGroup을 이용하여 Wait()와 Done()을 이용하여 모든 goroutine 수행을 답보
다중 CPU 처리
runtime.GOMAXPROCS(개수) //1: concurrent, 2: Parallel
개수는 Logical CPU 개수
24. Go Channel
데이터를 주고받는 통로, 별도의 lock을 통한 대기 없이 데이터를 동기화 하는 기법
채널명 := make(chan 자료형)
채널명 <- 값 (송신)
변수명 <- 채널 (수신)
package main
func main(){
ch := make(chan int)
go func(){
ch <- 30405 // 송신
}()
var i int
i = <- ch // 수신
println(i)
}
Go channel은 수신자와 송신자가 서로를 기다리므로 다음가 같은 sync도 가능
package main
import "fmt"
func main(){
done := make(chan bool) // Unbuffered Channel
go func(){
for i:=0; i< 10; i++{
fmt.Println(i)
}
done <- true
}()
<-done
}
Go channel buffering
위와 같은 예제는 수신자가 데이터를 받을때 까지 송신자가 채널에 묶임(Deadlock)
channel에 값을 보내고 다른 일을 수행할 수 있도록 buffered channel 지원
ch := make(chan int, 1)
ch <- 101
fmt.Println("hi")
Channel parameter
func sendChan(ch chan <- string){}
func recvChan(ch <-chan string){}
화살표를 지정하여 송신채널, 수신채널을 구분하여 전달 가능
Channel close
close(ch) // 송신은 불가능하지만 수신은 가능
for i:= range ch{
println(i)
}
if _, success = <-ch; !success{
println("ch is empty.")
}
Channel select
select를 이용하여 복수개의 channel의 데이터를 실행할 수 있다.
package main
import "time"
func main(){
done1:=make(chan bool)
done2:=make(chan bool)
go func(done chan bool){
time.Sleep(1 * time.Second)
done <- true
}(done1)
go func(done chan bool){
time.Sleep(2 * time.Second)
done <- true
}(done2)
EXIT:
for {
select {
case <- done1:
println("done1 완료")
case <- done2:
println("done2 완료")
break EXIT
}
}
}