[Go] 한 번에 정리하는 Golang 기본 개념 - 1편

2022. 10. 28. 15:37[ DevOps ]/[ Golang ]

Golang의 대표적인 특징 5가지

- 컴파일 언어

- 간결한 코드

- 미사용 메모리 자동 해제(GC)

- 편리한 동시성 코드 작성

- 멀티코어 프로세서 지원

 

 


 

package

go파일은 자바와 마찬가지로 패키지로 시작한다. 모든 Go 파일은 반드시 하나의 package를 포함해야 한다. main 패키지는 특수한 패키지로 간주된다. main 패키지는 프로그램이 시작하는 코드가 포함된 패키지를 의미한다.

 

대부분 다른 언어와 마찬가지로 import를 통해 다른 패키지를 가져올 수 있다. 이때 모든 패키지를 한 번에 다 가져오면 프로그램이 커지고 느려지기 때문에 필요한 패키지만 가져와야 한다. (실제 import 한 패키지는 컴파일 타임에 가져온다.) 자바와 다르게 go에서 import 해 온 함수를 사용할 때에는 반드시 "<패키지명>.<함수명>" 형태로 호출해야 한다.

 

package main

import (
	"fmt"
	"math"
	"strings"
)

func main() {
	fmt.Println("Hello", "世界")
	fmt.Println(math.Floor(2.75))
	fmt.Println(strings.Title("go go"))
}

// Hello 世界
// 2
// Go Go

- main 함수가 포함된 패키지는 반드시 main package여야 한다.

- 패키지 전체를 import 하지 않고 사용할 부분만 import할 때에는 archive/tar와 같이 '/'를 사용하여 import 경로를 지정할 수 있다.

 

 

cf. 다른 언어와 다르게 go는 일반적으로 세미콜론(;)을 사용하지 않는다. 하지만 이는 선택일 뿐 사용하고 싶다면 얼마든지 가능하다.

 

 


 

go fmt

Go 컴파일러에는 go fmt('format'의 약자)라는 표준 코드 포맷팅 도구가 함께 제공된다. 보통 들여쓰기나 띄어쓰기와 같은 것들이 표준화되어 있으면 코드를 읽기 쉽다. 다른 언어에서는 개발자가 직접 코드 형식을 직접 정리해줘야 하지만 Go에서는 go fmt를 사용하여 코드 형식을 자동으로 정리할 수 있다.

 

cf. 사용법: $ go fmt <go file path>

go fmt 커맨드는 코드 형식을 정리해 준다. 필수 과정은 아니지만 권장된다.

 

 


 

문자열(string)과 룬(rune)

- Go에서의 문자열은 다른 언어와 마찬가지로 ""로 작성하며 일련의 바이트이다.

- 룬의 경우 ''로 작성하며 단일 문자를 나타낼 때 사용한다.

- 룬은 문자 그 자체를 저장하지 않고 해당 문자를 나타내는 숫자 코드를 저장한다.

 

package main

import "fmt"

func main() {
	fmt.Println('A')	// 65 출력
	fmt.Println('❤')	// 10084 출력
	fmt.Println('\n')	// 10 출력
	fmt.Println('훈')	// 54984 출력
	fmt.Println('뷁')	// 48577 출력
	fmt.Println('さ')	// 12373 출력
	fmt.Println(true)	// true 출력
	fmt.Println(3.141598713258237592432)	// 3.1415987132582375 출력
}

 

* Go의 룬은 '유니코드'를 기반으로 하기 때문에 지구상에 존재하는 거의 대부분의 언어와 문자를 표현할 수 있다.

 

 


 

정적 타입 언어 vs 동적 타입 언어

Go는 C, C#, C++, Java 등의 언어와 마찬가지로 정적 타입 언어이다. 즉, 컴파일시에 타입이 결정된다. 반대로 실행 중에 동적으로 타입이 결정되는 동적 타입 언어(예를 들어, JavaScript, Ruby, Python 등의 보통의 인터프리터 언어)도 있다. 

 

정적 타입 언어는 컴파일시에 타입을 결정하기 때문에 런타임시에 안정성을 높인다는 장점이 있고 동적 타입 언어는 런타임 도중에도 타입에 대한 결정을 끌고 갈 수 있기 때문에 많은 선택의 여지가 있다는 장점이 있다.

 

 

타입

아래는 Golang에서 사용되는 대표적인 타입이다. go는 강타입 언어이기 때문에 타입에 대해 정확히 알아두고 사용해야 한다.

 

int - int int8 int16 int32 int64 및 uint uint8 uint16 uint32 uint64 uintptr 등이 있음
- 기본 int는 signed 숫자 표현
- 컴퓨터 시스템이 32bit일 경우 int32를 사용하고 64bit 시스템일 경우 int64 사용
float64 - 부동 소숫점 숫자 표현 (12자리 정밀도)
- 64bit 사용
- 완벽하진 않지만 거의 정확에 가까운 숫자 표현 가능
bool - true 또는 false
string - 한번 생성되면 수정될 수 없는 Immutable 타입
- 일련의 바이트
rune - int32와 정확히 일치하지만 '유니코드'로 표현한다는 차이점만 있음
byte - uint8과 완전히 동일함

 

 


 

변수

1. 변수 선언

go에서의  변수 선언은 다른 언어들과 약간 다른 것 같다는 느낌을 받았다. 변수 선언은 "var <변수명> <타입>" 순으로 선언한다.

var quantity int
var length, x, y float64
var name string

- 위와 같이 초기화를 하지 않을 경우 기본적으로 할당되는 default 값은 아래와 같다.

1) int : 0

2) float64: 0

3) string: ""

4) bool: false

 

2. 변수 초기화

변수를 먼저 선언하고 값을 초기화한다.

quantity = 2
length, x, y = 1.2, 2.4, 5.6
name = "do ja"

- 다른 언어들과 다르게 여러개의 변수를 한 번에 초기화할 수 있다.

- 다른 언어와 다르게 선언한 언어는 반드시 어디선가 사용되어야 한다. 그렇지 않으면 컴파일 단계에서 실패한다.

 

 

3. 선언 및 초기화 

선언 및 초기화를 동시에 진행한다. 변수 선언 시 사용했던 구문에 = 를 통해 값을 할당하면 된다.

var quantity int = 2
var length, x, y float64 = 1.2, 2.4, 5.6
var name string = "do ja"

 

cf. 타입 추론

변수 선언 및 초기화를 할 때에만 한정하여 타입을 생략할 수 있다. 이때 초기화한 값의 타입을 추론하여 타입이 결정된다.

var quantity = 2
var length, x, y = 1.2, 2.4, 5.6
var name = "do ja"
	
fmt.Println(reflect.TypeOf(quantity))	// int
fmt.Println(reflect.TypeOf(length))	// float64
fmt.Println(reflect.TypeOf(x))		// float64
fmt.Println(reflect.TypeOf(y))		// float64
fmt.Println(reflect.TypeOf(name))	// string

 

 

cf. 단축 변수 선언

quantity := 2
length, x, y := 1.2, 2.4, 5.6
name := "do ja"
	
fmt.Println(reflect.TypeOf(quantity))	// int
fmt.Println(reflect.TypeOf(length))	// float64
fmt.Println(reflect.TypeOf(x))		// float64
fmt.Println(reflect.TypeOf(y))		// float64
fmt.Println(reflect.TypeOf(name))	// string

- 변수 선언과 동시에 초기화를 할 경우에만 사용 가능

- 초기화과정이 포함되기 때문에 타입 명시 불필요

- 변수 선언 과정이 포함되기 때문에 var 키워드 생략 가능

- 이 경우엔 "<변수명> := <초기값>" 형태로 선언

- 가장 많이 사용되는 방식

 

func main() {
	a := 1
	b, a := 2, 3
	a, c, d := 4, 5, 6
	fmt.Print(a, b, c, d)	// 4 2 5 6
}

단축 변수 선언은 선언과 초기화를 함께 진행하는 키워드이지만 위와 같이 2번 이상 등장하게 할 수 있다. 마치 같은 이름의 변수를 2번 이상 선언하는 것처럼 보일 수 있으나 a라는 같은 변수에 값 할당만 추가적으로 하게 된다. 단, 단축 변수 선언을 통해 선언한 변수를 또다시 단축 변수 선언으로 하려면 반드시 1개 이상의 새로운 변수와 함께 선언될 때에만 가능하다. 즉, 아래와 같이 코드를 작성할 경우 에러가 발생한다.

 

func main() {
	a := 1
	a := 2
	fmt.Print(a)	// 에러
}

 

 

 

타입 변환

- go는 완전히 강타입 언어이기 때문에 서로 다른 타입 간의 연산에 대해 제약이 많다.

서로 다른 타입 간의 연산이 완전히 불가능하다.

- 예를 들어, int * float64 연산시연산 시 에러 발생, int > float64 연산 시 에러 발생 등.

- 아래는 서로 다른 타입 간 연산을 위해 타입 변환을 하는 예제 코드이다.

quantity := 2
quantity2 := float64(quantity)
quantity3 := 6.543

fmt.Println(reflect.TypeOf(quantity))          // int
fmt.Println(reflect.TypeOf(quantity2))         // float64
fmt.Println(reflect.TypeOf(float32(quantity))) // float32
fmt.Println(reflect.TypeOf(int(quantity3)))    // int
fmt.Println(quantity * int(quantity3))         // 12

 

 


 

 

네이밍 컨벤션

- '변수, 함수, 타입'의 이름을 '대문자'로 시작하면 public 형태가 되기 때문에 '외부 패키지로 노출'하겠다는 의미가 된다.

- 소문자로 시작할 경우 Java에서의 default와 같고 동일한 패키지 내부에서만 접근 가능하게 된다.

- 기본적으로 카멜 케이스(Camel Case) 형태로 많이 사용한다.

- 변수 이름에 예약어를 사용할 수 있다. (예시: var bool bool = false) 이 경우 bool 이라는 타입이 bool이라는 변수에 가려지게 되므로 특별한 경우가 아니라면 사용하지 않는 것이 좋다.

 

 


 

다중 반환 함수와 Blank Identifier(_)

Go는 대부분의 다른 언어와 다르게 여러 개의 값을 반환할 수 있다.  이러한 다중 반환 함수를 사용하는 가장 일반적인 경우는 함수를 실행하는 도중에 문제가 발생했는지를 확인할 수 있는 추가적인 에러 값을 같이 반환하는 경우이다.

 

 

[예시]

package main

import (
	"fmt"
	"net/http"
	"os"
	"strconv"
)

func main() {
	bool, err1 := strconv.ParseBool("ture")
	fmt.Println(bool)
	fmt.Println(err1)

	file, err2 := os.Open("not_exists_file.txt")
	fmt.Println(file)
	fmt.Println(err2)

	response, err3 := http.Get("http://golang.site/")
	fmt.Println(response)
	fmt.Println(err3)
}

// false
// strconv.ParseBool: parsing "ture": invalid syntax
// <nil>
// open not_exists_file.txt: no such file or directory
// <nil>
// Get "http://golang.site/": dial tcp: lookup golang.site on 169.254.169.254:53: dial udp 169.254.169.254:53: connect: no route to host

 

- err1: 문자열을 bool 타입으로 변경할 수 없는 경우 에러

- err2: 파일을 열수 없는 경우 에러

- err3: http 요청을 보낼 수 없는 경우 에러

 

- 다중 반환 함수의 리턴 값을 받지 않을 경우 빌드 자체가 안된다.(컴파일 타입 에러 발생) 따라서 다중 반환 함수의 리턴 값을 모두 받아줘야 한다.

- go에서는  선언한 변수를 사용하지 않을 경우 컴파일 에러가 발생한다. 즉, 다중 반환 함수의 리턴값을 받아서 결국 어디선가 한 번은 사용해줘야 한다. go에서는 예외적인 경우로, 할당은 하지만 사용하지 않는 경우를 대비해 Blank Identifier(_)를 제공한다.

 

 

[Blank Identifier를 적용한 코드]

bool, _ := strconv.ParseBool("ture")
fmt.Println(bool)

file, _ := os.Open("not_exists_file.txt")
fmt.Println(file)

response, _ := http.Get("http://golang.site")
fmt.Println(response)


// false
// <nil>
// <nil>

 

 

[에러 처리]

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
)

func main() {
	bool, _ := strconv.ParseBool("ture")
	fmt.Println(bool)

	file, err := os.Open("not_exists_file.txt")
	fmt.Println(file)
	log.Fatal(err)

	response, _ := http.Get("http://golang.site")
	fmt.Println(response)
}

 

- 다중 반환 함수는 대부분 에러를 반환하는 데 사용되기 때문에 무시하고 Blank Identifier(_)를 사용해서 무시하고 지나가는 것보다 처리해야 할 경우가 생길 수도 있다.

- 위 코드는 log 패키지의 Fatal을 사용하여 에러가 발생했을 때 로그를 출력하고 프로세스를 종료시키는 예시이다.

 

 


 

 

Golang 컴파일 및 실행

cf. 간단하게 실행할 때에는 플레이그라운드(https://go.dev/play/)에서 연습 가능.

 

1. example.go 파일 작성

 

2. 컴파일 (go build)

- mac, linux에서는 example이라는 파일이 생성되고 윈도우에서는 example.exe 파일이 생성된다.

 

3. 실행

- mac, linux: $ ./example

- 윈도우: $ example.exe

- 컴파일된 실행파일만 있으면 go가 설치되지 않은 곳에서도 실행할 수 있다.

- 컴파일된 바이너리 파일을 생성하지 않고 바로 실행하려면 go run 커맨드를 사용한다.

 

 

cf. go 커맨드 정리

go fmt 소스코드를 Go 표준 형식에 맞게 정리. 필요하지 않은 import 등을 삭제됨.
go build  소스코드를 바이너리 파일로 컴파일하고 컴파일된 실행파일을 저장함.
go run 소스코드를 컴파일한 뒤 바로 실행함. 컴파일된 실행파일은 저장되지 않음.

 

 

cf. vscode에서 go 실행 시 아래와 같이 디버깅을 위한 설정 추가

- 프로젝트 디렉토리에서 .vscode/launch.json 생성

{
    "version": "0.0.1",
    "configurations": [
      {
        "name": "go vscode tty console config",
        "type": "go",
        "request": "launch",
        "mode": "debug",
        "console": "integratedTerminal",
        "program": "${workspaceFolder}"
      }
    ]
  }

 


 

표준 입출력

1. fmt 사용

package main

import "fmt"

func main() {
	var num1, num2 float32
	fmt.Scanln(&num1, &num2)	// 1.1 2.2
	fmt.Println(num1, num2)		// 1.1 2.2
}

 

2. bufio, os 사용

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"reflect"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	input, err := reader.ReadString('\n') // "1.1\n"
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reflect.TypeOf(input)) // string

	var trimedInput string = strings.TrimSpace(input) // "1.1"
	fmt.Println(trimedInput)                          // "1.1"
	inputFloat, _ := strconv.ParseFloat(trimedInput, 32)
	fmt.Println(inputFloat) // 1.100000023841858
}

 

 


 

반복문: for

func main() {
	for i := 1; i <= 5; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println()

	i := 1
	for i <= 5 {
		fmt.Print(i, " ")
		i++
	}
}

- for만 제공된다. 다른 언어처럼 while 같은 반복문은 없다.

- 다른 언어와 비슷하게, 첫 번째 for문에서 볼 수 있는 가장 왼쪽에 위치한 초기화식이나 가장 오른쪽에 있는 후처리 식은 생략할 수 있다.

- for()와 같이 다른 언어처럼 괄호로 묶는 것은 금지된다.

- 다른 언어와 다르게 for문에서 첫 번째 식인 초기화문에서는 변수 할당만 가능하다. 즉, 기존에 선언된 변수를 가져와서 사용할 수 없다.

 

cf. i++과 같은 구문은 Print(i++)와 같이 함수 안에서 사용할 수 없다.