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

2022. 11. 2. 13:40[ DevOps ]/[ Golang ]

함수

1. 기본 구조

func AplusB(a int, b int) int {
	return a + b
}

func PrintAplusB(a int, b int) {
	fmt.Print(a + b)
}

- 다른 언어와 다르게 타입의 위치만 반대로 된 형태이다.

 

 

2. 다중 반환 함수

func AcalB(a int, b int) (int, int, int) {
	return a + b, a - b, a * b
}

func main() {
	puls, minus, multi := AcalB(1032, 3124)
	fmt.Print(puls, minus, multi)	// 4156 -2092 3223968
}

- 반환 값이 2개 이상일 경우엔 return type을 괄호로 묶는 것은 필수이다.

 

func AcalB(a int, b int) (plus int, minus int, multi int) {
	return a + b, a - b, a * b
}

- 위와 같이 반환 값이 여러 개일 경우에 각 반환값이 의미하는 바를 명시할 수도 있다.

- 반환 값의 이름을 명시하는 목적은 개발자를 위한 것일 뿐이다.

 

 

2. 에러값 생성

func AdivB(a int, b int) (int, error) {
	if b <= 0 {
		return 0, fmt.Errorf("b(%d) is invalid.", b)
	}
	return a / b, nil
}

func main() {
	div, err := AdivB(8, 0)
	if err != nil {
		fmt.Print(err)	// b(0) is invalid.
	} else {
		fmt.Print(div)
	}
}

- 에러 값이 없을 경우엔 보통 nil을 반환한다.

 

cf. error 타입은 go에서 지정된 키워드이기 때문에 변수명으로 사용하지 않는 것이 좋다.

cf. Go의 함수는 오버로딩이 지원되지 않는다. 

 


 

 

Call-By-Value

go는 기본적으로 call-by-vlaue(pass-by-value) 언어이다. 따라서 함수가 호출될 때 인자값이 그대로  복사된다. 

 

 

포인터: Call-By-reference

https://zapiro.tistory.com/entry/%ED%8F%AC%EC%9D%B8%ED%84%B0pointer-%EB%B3%80%EC%88%98%EC%9D%98-%EA%B0%9C%EB%85%90

 

C/C++과 마찬가지로 & 연산자를 통해 변수의 주소값을 가져올 수 있다. & 연산자는 모든 변수에 적용할 수 있다. 즉, Go에서는 타입과 관계 없이 모든 변수의 주소 값을 가져올 수 있다.

 

func main() {
	var val1 int
	var val2 string
	var val3 float64
	var val4 bool

	fmt.Println(&val1, &val2, &val3, &val4) // 0xc000112000 0xc000110010 0xc000112008 0xc000112010
}

 

 

포인터 타입

- (복습) 포인터란 변수의 주소를 나타내는 값

- C에서와 마찬가지로 포인터라는 의미를 가지도록 별도의 타입이 존재 (* + <type명> 형태)

- 포인터 변수는 타입과 관계없이 같은 크기를 가진다. (컴파일러에 따라 크기는 달라짐)

- 아래 예시 코드에서는 모든 포인터 변수의 크기가 8 바이트이다.

 

func main() {
	var val1 int
	var val2 string
	var val3 float64
	var val4 bool

	fmt.Println(reflect.TypeOf(&val1)) // *int
	fmt.Println(reflect.TypeOf(&val2)) // *string
	fmt.Println(reflect.TypeOf(&val3)) // *float64
	fmt.Println(reflect.TypeOf(&val4)) // *bool

	val1Ptr := &val1
	val2Ptr := &val2
	val3Ptr := &val3
	val4Ptr := &val4

	fmt.Println(unsafe.Sizeof(val1Ptr)) // 8
	fmt.Println(unsafe.Sizeof(val2Ptr)) // 8
	fmt.Println(unsafe.Sizeof(val3Ptr)) // 8
	fmt.Println(unsafe.Sizeof(val4Ptr)) // 8
}

 

- 포인터 변수에 *를 붙이면 포인터가 가리키고 있는 변수의 값을 가져올 수 있다.

- *를 통해 값을 변경하면 원본 변수의 값도 같이 변경된다. (reference 참조 이기 때문)

 

func main() {
	val1 := 4
	val1ptr := &val1
	fmt.Println(val1)     // 4
	fmt.Println(val1ptr)  // 0xc000120000
	fmt.Println(*val1ptr) // 4

	*val1ptr = 8
	fmt.Println(*val1ptr) // 8
	fmt.Println(val1)     // 8 <- reference 참조이기 때문에 원래 변수도 변한다.
}

 

- 포인터 변수는 함수의 return type으로도 사용될 수 있다.

- 아래와 같이 Go는 다른 언어와 달리 함수의 로컬 변수 포인터를 반환할 수 있다. 함수의 스코프는 벗어나지만 해당 변수의 포인터를 가지고 있는 동안에는 해당 변수의 값에 접근할 수 있다.

 

func createPointer() *int {
	val := 26
	return &val
}

func main() {
	var ptr *int = createPointer()
	fmt.Println(ptr)	// 0xc00001c030
	fmt.Println(*ptr)	// 26
}

 

- 함수의 인자로 포인터를 전달할 때에는 매개변수 타입을 포인터 타입으로 지정하면 된다.

 

func printPointer(num *int) {
	fmt.Println(*num)	// 10
}

func main() {
	var val int = 10
	printPointer(&val)
}

 

 


 

 

Package

공통 로직을 수행하는 함수들을 별도의 패키지로 분리할 수 있다. 공통 로직을 패키지로 분리시켜 모듈화해 두면 서로 다른 프로그램끼리 동일한 코드를 사용할 수 있게 된다. 즉, 코드 공유(함수 공유)를 위해서 패키지화하는 목적도 있다.

 

 

workspace

go의 기본 workspace는 홈 디렉토리 아래 위치한다. 

 

cf. OS별 홈 디렉토리

- 윈도우: C:\User\<username>

- Mac: /User/<username>

- Linux: /home/<name>

 

 

Go 기본 디렉토리

https://www.oreilly.com/library/view/head-first-go/9781491969540/ch04.html

 

1) bin: '컴파일된' 실행 가능한 바이너리 프로그램(실행파일)

2) pkg: '컴파일된' 바이너리 '패키지' 파일

3) src: 소스코드 (패키지 소스코드 등)

 

* 각 패키지의 코드는 src 디렉토리 하위 패키지명의 디렉토리로 생성된다. 예를 들어 fmt 패키지는 src/fmt/에 생성된다. 각 패키지 디렉토리는 하나 이상의 소스코드 파일(.go)을 포함해야 한다.

* import 하는 패키지 코드를 찾을 때에는 항상 src 디렉토리에서 찾게 된다.

 

 

패키지 생성 실습

1. GOROOT 또는 GOPATH로 설정된 디렉토리의 src/ 하위에 이동

2. 생성할 패키지명과 동일한 디렉토리 생성 

    - 예: go/src/greeting

3. go/src/greeting 디렉토리에 go file 작성

     - greeting.go

package greeting

import "fmt"

func Hello() {
	fmt.Println("Hello!")
}

 

4. 새로 만든 패키지 가져오기

package main

import "greeting"

func main() {
	greeting.Hello()	// Hello!
}

- 위 코드는 GOROOT 상에 존재하는 프로젝트가 아니어도 상관없음.

- go run 커맨드로 컴파일 및 실행

 

 

cf. GOROOT 환경변수는 go env 커맨드로 확인 가능

- 기본설정 시 리눅스의 경우 GOROOT는 $HOME/go로 설정됨.

 

The GOPATH environment variable is used to specify directories outside of $GOROOT that contain the source for Go projects and their binaries.

 

 

* 동작원리

- 위와 같이 패키지를 만들고 실행하면 import 문의 "greeting"을 보고 GOROOT의 src/greeting 패키지에서 소스코드를 찾는다. 그리고 해당 코드가 컴파일되고 import 되면 greeting 패키지의 함수가 호출된다.

- 따라서 반드시 패키지 명과 디렉토리 명을 동일하게 작성해야 한다.

 

 

cf. 패키지명 컨벤션

- 패키지명은 소문자로만 작성한다.

- 두 단어 이상이 될 경우 카멜 케이스도, 스네이크 케이스도 사용하지 않으며 두 단어를 붙인다 (ex. strconv)

- 축약어를 사용한다. (ex. fmt: format의 약자)

 

 

cf. 패키지 그룹화

src 디렉토리 하위에 디렉토리  경로 자체가 import 할 패키지 경로가 된다. 예를 들어, archive 패키지의 tar 패키지는 src/archive/tar 디렉토리에 위치한다. 

 

 

패키지 실행파일 생성(go install)

- go install 커맨드는 go build 커맨드와 비슷하게 .go 파일을 컴파일한다.

- go install 커맨드는 패키지 코드를 컴파일하며 결과물을 일반 패키지는 pkg 디렉토리에, 실행파일은 bin 디렉토리에 저장한다.

- 커맨드: go install 또는 go install $GOROOT/go/src/<package_name>

- 저장될 패키지 go 파일 경로: $GOROOT/go/src/<package_name>

- 저장될 패키지 실행파일 경로:  $GOROOT/go/pkg/<package_name>

- pkg 디렉토리에는 컴파일된 실행파일이 생성된다.

 

cf. go build vs go install

- go build는 소스파일(.go)명을 그대로 실행파일 이름을 지정한다.

- go install은 .go 파일이 포함된 디렉토리의 이름을 기반으로 실행파일 이름을 지정한다.

- go build는 실행파일을 현재 디렉토리에 생성한다.

- go install은 bin 디렉토리에 실행파일을 생성한다.

- go install은 커맨드를 실행하는 위치와 무관하게 $GOROOT/go/src의 소스코드를 컴파일한다.

 

 

패키지 배포 및 네이밍

보통 Git과 같은 원격 저장소에 패키지를 배포한다고 가정했을 때, 이를 사용하는 사람 입장에서는 패키지 명을 직접 지정해줘야 한다. 만약 사용자가 이전에 만들어 둔 패키지 명과 중복될 수도 있다. 따라서 보통의 경우엔 원격 저장소의 디렉토리 구조를 그대로 패키지 구조로 사용한다. (사실 이러한 방식은 보편적으로 사용되는 방식이다)

 

[예시]

- 원격 저장소 주소: http://github.com/example1/package1

- 패키지 구조: go/src/github.com/example1/package1

- import 작성: import "github.com/example1/package1"

 

 

패키지 다운로드(go get)

원격 저장소에 있는 패키지를 쉽게 다운로드할 수 있는 go get 커맨드가 제공된다. 

 

* 예시: go get github.com/example1/package1

 

- go get 커맨드는 git 커맨드 기반으로 동작하기 때문에 git이 설치되어 있어야 한다.

- go get 커맨드를 통해 https:// 부분은 생략할 수 있다. 

- go get 커맨드는 작업 공간의 src 디렉토리 하위에 자동으로 패키지를 설정한다.

 

 


 

패키지 주석과 함수 주석

1. 패키지 주석

- 소스코드 상에서 package 선언문 위에 작성된 주석 (//)

- 주석 내용은 "Package"라는 단어로 시작해야 함

 

2. 함수 주석

- 소스코드의 함수 바로 위에 작성된 주석 (//)

- 주석 내용은 "함수 이름"으로 시작해야 함

 

* 패키지 주석과 함수 주석은 go doc 커맨드를 사용할 때 보여진다.

- 예시1 - 함수 주석: go doc strconv ParseFloat

- 예시2 - 패키지 주석: go doc <github_url>

 

 


 

GOPATH

코드가 기본 작업 공간(GOROOT) 외 디렉토리에 저장된 경우, Go 컴파일러가 해당 위치에서 코드를 찾을 수 있도록  설정해주어야 한다. 즉, GOPATH 환경변수 설정으로 작업 공간 위치를 변경할 수 있다.

 

- Mac, Linux: .bashrc 파일에 작업 디렉토리를 명시하여 GOPATH 노출

ex) echo "export GOPATH=<작업공간>" >> ~/.bashrc

 


 

상수(constant)

- 다른 언어와 마찬가지로 상수(constant)를 가진다.

- 상수는 이름을 가진 불변(immutable)의 값이다.

- var 키워드 대신 const 키워드를 사용한다.

- 선언과 동시에 반드시 초기화해야 한다.

- 변수처럼 := 키워드를 사용할 수 없다.

- 변수와 마찬가지로 타입을 생략할 수 있다. (타입추론)

- 함수 내에서 선언할 수도 있지만 대부분 패키지 레벨에서 선언한다.

- 상수 이름이 대문자로 시작할 경우 함수와 마찬가지로 패키지 외부에 노출시킬 수 있다.

 

package main

import "fmt"

const DaysInWeek int = 7

func weeksToDay(weeks int) int {
	return weeks * DaysInWeek
}

func main() {
	fmt.Println(weeksToDay(2))
}

 

 

 

 

 

 

 

Reference

- Go docs, https://go.dev/doc/tutorial/compile-install