Go (1) - Hello World, 변수, 표준 입출력, 연산자, 함수, 상수

개발 환경 구축

 

다운로드

  1. 1. GoLang(https://go.dev/dl/) 공식 홈페이지에서 OS에 맞는 파일을 다운로드
  2. IDE에서 Go 확장 프로그램 설치 

 

코드 실행 방법 (VSCode)

  1. main.go 파일 작성
  2. 터미널에서 해당 패키지 경로로 이동
  3. 루트 경로로 이동 후 모듈 초기화
  4. 실행 파일 생성 (윈도우는 .exe 파일)
  5. 실행 
// 3. 모듈 초기화
go mod init 패키지명

// 4. 실행파일 생성
go build

// 5. 실행
./실행파일명

 

 

컴퓨터 원리

 

  1. 프로그램 로드
    • 운영체제는 프로그램 실행 파일을 메모리에 복사
  2. 데이터 로드 및 캐싱
    • 메모리에서 연산에 필요한 데이터를 CPU 내부의 캐시로 복사
  3. 연산 및 저장
    • CPU는 연산에 사용할 데이터를 레지스터로 복사
    • 레지스터는 실제 연산이 수행되는 특수한 데이터 공간
    • 그 다음 줄 연산 실행
  4. 프로그램 종료 때까지 2~3 반복

 

프로그래밍 언어

 

컴퓨터가 알 수 있는 건 오직 0과 1밖에 없습니다. 예를 들어 3과 4를 더하라는 의미의 'ADD 3 4' 라는 명령을 내리고 싶다면 3, 4 를 0과 1뿐인 2진수로 표현하고 ADD 라는 명령어도 규칙을 정해 기계어로 표현해야합니다. 초기 프로그래머들은 천공 카드에 구멍을 뚫는 방식으로 기계어를 작성해 컴퓨터에 명령을 내렸는데, 이는 굉장히 힘든 일이었습니다.

 

그래서 'ADD 3 4' 를 기계어로 변환시켜주는 프로그램을 만들었는데, 이것이 어셈블리어입니다. 어셈블리어는 기계어와 1:1로 대응되는 언어로 인간이 쉽게 읽고 쓸 수 있는 ADD, SUB 같은 문자로 표현할 수 있었습니다. 그러나 어셈블리어는 기계어보다는 상대적으로 쉬웠지만, 칩셋마다 명령을 새로 익혀야 하는 불편함이 있었습니다. 그리고 프로그래밍에 코딩 양도 많아 전체 동작을 이해하기도 어렵고 버그 발생 확률도 높았습니다. 또한 다른 아키텍처로 쉽게 이식할 수도 없었습니다.

 

그래서 더 인간의 언어 표현법에 가까운, 즉 더 프로그래밍하기 편한 고수준 프로그래밍 언어가 등장했습니다. 고수준 언어의 등장으로 인간 친화적으로 코드를 작성할 수 있게 됐고, 필요한 코드의 양도 대폭 줄어들게 되었습니다.

어떤 프로그래밍 언어로 작성하든 컴퓨터가 명령을 실행하려면 결국 기계어로 변환되어야 합니다. 하지만 고수준 언어는 어셈블리어와 달리 기계어로 바로 변환될 수 없기 때문에 기계어로 변환해주는 프로그램인 컴파일러가 필요합니다.

 

프로그래밍 언어의 구분

 

1. 정적 컴파일 언어 vs 동적 컴파일 언어

미리 기계어로 변환해두었다가 사용하는 방식의 언어를 정적 컴파일 언어라고 합니다. 그리고 이렇게 기계어로 변환해둔 파일을 실행 파일이라고 하며, 윈도우에서는 .exe 파일이 미리 기계어로 변환된 실행 파일입니다. 실행할 때 변환 과정이 필요 없어서 빠르고, 타입 에러를 컴파일 시점에서 발견할 수 있어 타입 안전성이 뛰어납니다.

 

반면에 실행 시점(runtime)에 기계어로 변환하는 방식의 언어를 동적 컴파일 언어라고 합니다. 동적 컴파일 언어는 기계어로 미리 변환해두지 않고 실행할 때 변환하기 때문에 정적 컴파일 언어보다 더 느리게 동작합니다. 이런 동적 컴파일 언어는 정적 컴파일 언어보다 나중에 개발되었는데, 이는 정적 컴파일 언어의 단점을 극복하기 위해서입니다.

 

칩셋과 운영체제마다 0과 1로 된 바이너리 코드를 표현하는 형식이 다르기 때문에 기계어로 변환할 때 각 환경에 맞게 변환해줘야 합니다. 그런데 동적 컴파일 언어는 프로그램 실행 시점에 환경에 맞는 기계어로 변환되기 때문에 이런 번거로움 없이 하나의 코드로 모든 플랫폼에서 실행 가능합니다.

 

Go 언어는 정적 컴파일 언어입니다.

 

2. 약 타입 언어 vs 강 타입 언어

프로그래밍 언어를 나눌 때 타입 검사를 강하게 하는 언어와 그렇지 않은 언어로 나눌 수도 있습니다. 타입 검사를 강하게 하는 언어를 강 타입 언어 또는 정적 타입 언어라 부르고 타입 검사를 약하게 하는 언어를 약 타입 언어 또는 동적 타입 언어라고 말합니다.

예를 들어 문자열인 "12" 와 숫자인 12 를 더했을 때 프로그래밍 언어에 따라 "1212" 로 만들거나 24로 계산하기도 합니다.

 

약 타입 언어는 규칙이 관대해 더편하게 코딩할 수 있는 장점이 있는 반면 예기치 못한 버그를 발생시킬 수 있습니다. 강타입 언어는 사용하기 까다롭지만 타입 검사를 언어 자체에서 해주기 때문에 타입으로 생길 수 있는 문제를 미연에 방지할 수 있습니다.

 

Go 언어는 자동 타입 변환까지도 지원하지 않는 최강 타입 언어입니다.

 

3. 가비지 컬렉터(GC) 유무

가비지 컬렉터란 쓰레기 청소부란 뜻으로 메모리에서 불필요한 영역을 치워줍니다. 가비지 컬렉터가 없는 언어는 프로그래머가 메모리 할당과 해제를 책임져야 하는데, 할당한 메로리를 해제하지 않아 메모리 누수 현상이 발생하거나, 이미 해제된 영역을 다시 해제해 버그가 발생하는 문제가 있습니다. 

 

반면 가비지 컬렉터가 있으면 메모리 해제를 자동으로 해주기 때문에 메모리 관련 문제가 줄어든다는 장점이 있지만, 메모리 청소에 CPU 성능을 사용한다는 문제가 있습니다. 그래서 대체로 가비지 컬렉터가 없는 프로그래밍 언어가 더 빠른 성능을 자랑합니다.

 

Go 언어는 매우 발전된 형태의 가비지 컬렉터를 제공합니다.

 

 

Hello World

 

Go 언어의 역사

 

Go 언어는 2009년 발표된 오픈 소스 프로그래밍 언어입니다. 무료로 사용할 수 있고, 누구나 내부 구조를 살펴볼 수 있고, 누구나 Go 언어 발전에 이바지 할 수 있습니다. 현재 Go 언어는 지속적인 업데이트를 하고 있고 글을 쓰는 날짜 기준으로 1.24 버전까지 공개되었습니다.

 

아래의 표를 확인해보면 2024년 1분기 기준 Go 언어가 오픈 소스 허브인 깃허브에 가장 많이 PR(Pull Request)한 언어 랭킹 3위에 해당하는 것을 확인해볼 수 있습니다.

출처: https://madnight.github.io/githut/#/pull_requests/2024/1

 

또한 2024 Stack Overflow Survey의 Admired and Desired 항목의 상위 15개 언어 리스트를 보면 지난 1년간 Go 언어를 사용한 개발자 중 67.7% 가 만족하고 있으며 23.1% 가 배우기를 희망하고 있다는 것을 확인해 볼 수 있습니다.

출처: 2024 Stack Overflow Survey - Admired and Desired

 

Go 언어의 특징

 

Go 언어의 특징을 간단하게 표로 요약해 보면 다음과 같습니다.

 

Hello World

 

package main		// 1

import "fmt"

func main() {
	fmt.Println("hello World")		// 2
}

 

1. package main

Go 언어의 모든 코드는 반드시 패키지 선언으로 시작해야 합니다. main 패키지는 프로그램 시작점(entry point)을 포함하는 특별한 패키지입니다. main() 함수가 있는 패키지만 main 이라는 패키지명을 사용할 수 있으며, main() 함수가 없으면 실행 파일을 만들 수 없고, 다른 패키지에서 외부 패키지로 사용됩니다.

2. fmt.Println("hello World")

fmt 패키지는 표준 입출력을 다루는 내장 패키지로, 표준 입출력으로 텍스트를 출력하거나 입력받을 때 사용합니다.

Pintln() 은 표준 출력으로 문자열을 출력하는 함수입니다. 표준 출력이란 터미널 화면을 의미합니다.

 

 

변수

 

프로그래밍에서 변수(variable)는 값을 저장하는 메모리 공간을 가리키는 이름입니다. 컴퓨터의 입장에서 프로그램은 '메모리에 있는 데이터를 언제 어떻게 변경할지를 나타낸 문서' 입니다. 이때 변수를 이용하면 쉽고 효과적으로 메모리에 있는 데이터를 조작할 수 있습니다.

 

변수 선언

 

변수를 사용하려면 먼저 변수를 선언해야 합니다. 변수 선언이란 컴퓨터에게 값을 저장할 공간을 마련하라고 명령을 내리는 것으로 이것을 메모리 할당이라고 부릅니다.

 

 

 

변수는 4가지 속성을 가집니다.

  1. 이름: 변수명을 통해 값이 저장된 메모리 공간에 접근할 수 있습니다.
  2. 값: 변수가 가리키는 메모리 공간에 저장된 값입니다.
  3. 주소: 변수가 저장된 메모리 공간의 시작 주소를 말합니다.
  4. 타입: 변숫값의 형태를 말합니다.

위 그림과 같이 변수 a를 선언하면 컴퓨터는 메모리에 int 타입 크기에 해당하는 공간을 할당하고 그 공간이 위치한 메모리 시작 주소를 a로 지칭합니다. 그리고 메모리 공간에 10이라는 값을 복사합니다.

 

변수명은 문자, _, 숫자를 사용해 지을 수 있지만 숫자는 첫 글자에 올 수 없습니다.
코드가 기계어로 변환되면 변수명은 모두 사라지고 대신 메모리 주솟값으로 대체됩니다.

 

 

Go 언어에서는 편의를 위해서 다음과 같이 여러 형태의 변수 선언을 지원하고 있습니다.

package main

import "fmt"

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

 

1. 기본 형태

2. 초깃값을 생략한 형태로, 타입별 기본값으로 대체

3. 타입을 생략한 형태로, 우변 값의 타입으로 지정

정수는 int, 실수는 float64가 기본 타입입니다.

4. 선언 대입문(:=)을 사용한 형태로, var 키워드와 타입을 생략

 

Warning

선언하고 사용하지 않는 변수가 있으면 컴파일 에러를 발생하기 때문에 _(밑줄)을 사용해서 반드시 무효화시켜야 합니다.

func main() {
    var a int
    var b int
    
    _, err := fmt.Scan(&a, &b)		// 무효화
    if err != nil {
    	fmt.Println(err)
    } else {
    	fmt.Println(a, b)
    }
}

 

변수 타입

 

◎ 숫자 타입

부호 없는 정수 숫자는 uint, 부호 있는 정수 숫자는 int, 실수는 float, 복소수는 complex로 나타내고 뒤에 붙는 숫자는 비트 단위를 나타냅니다. 

 

뒤에 숫자가 없는 타입은 컴퓨터에 따라 달라지지만 오늘날에는 대부분 64비트이기 때문에 각각 uint64, int64와 같습니다.

 

◎ 그외 타입

그외 타입인 boolean, string, array, slice, 구조체, 포인터, 함수 타입, 인터페이스, list, ring, map, channel은 뒤에서 다시 다루겠습니다.

boolean은 false, string은 "", 그외는 nil을 기본값으로 가집니다.

 

 

타입 변환

 

Go 언어는 강 타입 언어 중에서도 강하게 타입 검사를 하는 언어이기 때문에 연산이나 대입에서 타입이 다르면 에러가 발생합니다.

또한 자동 타입 변환도 되지 않기 때문에 직접 명시해야 합니다.

 

package main

func main() {
    a := 3	// int
    var b float64 = 3.5
    
    c := a + b	// error
    
    var d int = int(b)
    e := a + d
}
실수 타입에서 정수 타입으로 타입 변환하면 소수점 이하 숫자가 반올림되지 않고 버려집니다.

 

큰 범위를 갖는 타입에서 작은 범위를 갖는 타입으로 변환하면 값이 달라질 수 있습니다.

 

 

변수의 범위

 

변수는 자신이 속한 중괄호 { } 범위 안에서만 유효하며, 어떤 중괄호에도 속해 있지 않은 변수는 전역 변수로 패키지 내에서는 어디서나 접근할 수 있습니다.

package main

var g int = 10		// 전역 변수

func main() {
    var l int = 20	// 지역 변수
}

 

 

표준 입출력

 

프로그램과 사용자는 입력과 출력을 통해서 상호작용을 합니다. 입력은 키보드, 파일, 네트워크 등에서 받을 수 있고, 출력도 화면, 파일, 네트워크 등으로 전송할 수 있습니다. 운영체제는 이런 다양한 상황에 간편하게 입출력 처리를 할 수 있는 표준 입출력 스트림(Stdin, Stdout, Stderr)을 제공하고 이를 Go 언어에서는 fmt 패키지를 사용해서 간편하게 처리할 수 있습니다.

표준 입출력은 리디렉션(>, <)을 통해 파일로 입출력을 변경할 수 있습니다.

 

표준 출력

 

fmt 패키지는 3가지 표준 출력용 함수를 제공하는데, 일반적으로 표준 출력을 변경하지 않았다면 화면이 표준 출력 장치입니다.

  • Print() : 출력값 사이에 공란이 없고, 출력이 끝나고 개행을 하지 않습니다.
  • Println() : 출력값 사이에 공란이 있고, 출력이 끝나면 개행을 합다.
  • Printf() : 사용자 서식에 맞춰서 입력값을 출력합니다.
package main

import "fmt"

func main() {
    a := 10
    var b float64 = 10.55
    
    fmt.Print("a:", a, "b:", b)
    fmt.Println("a:", a, "b:", b)
    fmt.Printf("a: %d b: %f\n", a, b)
}

 

표준 입력

 

fmt 패키지는 3가지 표준 입력용 함수를 제공하는데, 일반적으로 표준 입력을 변경하지 않았다면 키보드가 표준 입력 장치입니다.

  • Scan() : 한 번에 여러 값을 입력 받을 때는 변수 사이를 공란을 두어 구분합니다.
  • Scanln() : 한 줄을 입력받습니다.
  • Scanf() : 사용자 서식에 맞춘 입력을 받습니다.
package main

import "fmt"

func main() {
    var a int
    var b int
    
    n, err := fmt.Scan(&a, &b)
    if err != nil {
    	fmt.Println(n, err)
    } else {
    	fmt.Println(n, a, b)
    }
}
표준 입력 함수를 사용할 때에는 변수 앞에 &를 붙여서 변수의 메모리 주소를 인자로 넘겨야 합니다.

 

 

연산자

 

산술 연산자

 

산술 연산자는 숫자 연산을 하는 연산자입니다.

  • 사칙 연산: +(덧셈), -(뺄셈), *(곱셈), /(나눗셈), %(나머지)
  • 비트 연산: &(AND), |(OR), ^(XOR), &^(클리어)
  • 시프트 연산: <<, >>

 

비교 연산자

 

비교 연산자는 양변을 비교해서 조건에 만족하면 true, 만족하지 못할 경우 false를 반환하는 연산자입니다.

  • ==(같다), !=(다르다), <(작다), >(크다), <=(작거나 같다), >=(크거나 같다)

 

논리 연산자

 

논리 연산자는 boolean 피연산자를 대상으로 연산해 결과로 true, false 를 반환하는 연산자입니다.

  • &&(AND), ||(OR), !(NOT)

 

대입 연산자

 

대입 연산자는 우변값을 좌변(메모리 공간)에 복사합니다.

package main

func main() {
    var a int = 10	// 대입
    var b int = 20
    
    a, b = b, a		// 복수 대입
    
    a = a + 2		// 복합 대입
}

 

 

그 외 연산자

 

주소 연산자(&)는 실제 값에 붙이면 해당 값의 메모리 주소(포인터)를 반환하고,
역참조 연산자(*)는 포인터에 붙이면 그 포인터가 가리키는 실제 값을 반환합니다.

 

 

함수

 

함수 정의

 

함수는 함수 키워드, 함수명, 매개변수, 반환 타입, 함수 코드 블럭으로 구성됩니다.

 

첫 글자가 대문자인 함수는 패키지 외부로 공개되는 함수입니다.
매개변수와 반환 타입은 없는 경우 비워둡니다.

 

멀티 반환 함수

 

함수는 값을 여러 개 반환할 수 있습니다. 반환값이 여럿일 때는 반환 타입들을 소괄호로 묶어서 표현합니다.

package main

func Divide(a, b int) (int, bool) {
    if b == 0 {
    	return 0, false
    }
    return a / b, true
}

func main() {
    c, success := Divide(9, 3)
}
매개변수는 타입이 같으면 한 번만 선언할 수 있습니다.

 

반환 타입에 변수명 지정

 

함수 선언부에 반환 타입을 적을 때 변수명까지 지정해주면, return문으로 해당 변수를 명시적으로 반환하지 않아도 값을 반환할 수 있습니다.

package main

func Divide(a, b int) (result int, success bool) {
    if b == 0 {
    	result = 0
        success = false
        return
    }
    result = a / b
    success = true
    return
}

func main() {
    c, success := Divide(9, 3)
}

 

 

상수

 

상수는 변하지 않는 값을 말합니다. 변수는 대입문을 통해서 값을 수시로 바꿀 수 있지만 상수는 초기화된 값이 변하지 않습니다. 정수, 실수, 문자열 등 기본 타입값들만 상수로 선언될 수 있습니다. 상수 선언 방식은 변수(variable)를 뜻하는 var 키워드 대신에 상수(constant)를 뜻하는 const 키워드를 사용한다는 점을 제외하고 변수와 비슷합니다. 상수명 규칙 또한 변수명과 같습니다.

함수 외부에 선언되어 있고 첫 글자가 대문자인 상수는 패키지 외부로 공개되는 상수입니다.

 

그러나 변수의 선언과 한 가지 다른 점은 상수 선언 시 타입을 명시하지 않을 수 있다는 것입니다. 이 경우 타입 없는 상수가 되고, 타입이 정해지지 않은 상태로 사용됩니다. 타입 없는 상수는 변수에 복사될 때 타입이 정해지기 때문에 여러 타입에 사용되는 상숫값을 사용할 때 편리합니다.

 

package main

import "fmt"

const PI = 3.14			// 타입 없는 상수

func main() {
    const C int = 10		// 타입 선언
    
    var a int = PI * 100
    
    fmt.Println(&PI)		// error
}

 

위 코드에서 & 연산자를 사용하여 상수의 주소값을 출력하는 코드를 작성했지만 에러가 발생했습니다. 상수는 값으로만 동작하기 때문에 변수와 달리 메모리 주소를 가지지 않습니다.

 

상수와 리터럴

 

컴퓨터에서 리터럴이란 고정된 값, 값 자체로 쓰인 문구라고 볼 수 있습니다. 

 

Go 언어에서 상수는 리터럴과 같이 취급합니다. 그래서 컴파일될 때 상수는 리터럴로 변환되어 실행 파일에 쓰입니다.

상수 표현식 역시 컴파일 타임에 실제 결과값 리터럴로 변환되기 때문에 상수 표현식 계산에 CPU 자원을 사용하지 않습니다.

상수의 메모리 주소값이 없는 이유 역시 컴파일 타임에 리터럴로 전환되어서 실행 파일에 값 형태로 쓰이기 때문입니다.

 

iota 키워드

 

상수는 임의의 숫자에 의미를 부여하여 사용되는 코드값으로 사용할 수 있습니다.

const Pig int = 0
const Cow int = 1
const Dog int = 2

 

이렇게 1씩 증가하는 코드값으로 정의할 때 iota 키워드를 사용하면 편리합니다. iota는 크리스 알파벳의 9번째 글자로, 아주 작은양을 뜻합니다. 상수 목록을 const와 소괄호 ()로 묶고 iota를 사용하면 0부터 1씩 차례로 증가하며 값이 초기화됩니다. 만약 첫 번째 값과 똑같은 규칙이 계속 적용된다면 타입과 iota를 생략할 수 있습니다.

const (
    Pig int = iota		// 0
    Cow int = iota		// 1
    Dog int = iota		// 2
)

const (
    Pig int = iota + 1		// 1
    Cow				// 2
    Dog				// 3
)