Go 패키지(Package)
- Go언어는 패키지를 통해 코드의 모듈화, 재 사용 기능을 제공한다.
- Go언어는 패키지를 사용해서 작은 단위의 컴포넌트(Component)를 작성하고, 이러한 작은 패키지를 활용하여프로그램을 작성할 것을 권장한다.
- Go언어에 사용하는 표준 패키지는 https://golang.org/pkg/ 에 자세하게 설명되어 있다.
Main 패키지
일반적으로 패키지는 라이브러리로서 사용되지만, 'main' 패키지는 Go 컴파일러(Compiler)에 의해 특별하게 인식된다.
패키지 명이 main인 경우, 컴파일러는 해당 패키지를 공유 라이브러리가 아닌 실행(executable) 프로그램으로 만든다.
그리고 이 main 패키지 내의 main() 함수가 프로그램의 시작점인 Entry Point가 된다.
패키지를 공유 라이브러리로 만들 시, main 패키지나 main 함수를 사용해서는 안된다.
패키지 Import
다른 패키지를 프로그램에서 사용하기 위해서는 import를 사용하여 패키지를 포함시킨다.
아래의 예제에서는 Go의 표준 라이브러리인 fmt 패키지를 사용하기 위해서, import "fmt" 와 같이 해당 패키지를
포함시킬 것을 선언해 준다.
import 후에는 fmt패키지의 Println() 함수를 호출하여 사용할 수 있다.
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
패키지를 import할 시, Go 컴파일러는 GOROOT 또는 GOPATH 환경변수를 검색하는데, 표준 패키지는 GOROOT/pkg에서 그리고 사용자 패키지나
3rd Party 패키지의 경우 GOPATH/pkg에서 패키지를 찾게 된다.
GOROOT 환경변수는 Go 설치 시 자동으로 시스템에 설정되지만, GOPATH는 사용자가 지정해 주어야 한다.
GOPATH 환경변수는 3rd Party 패키지를 갖는 라이브러리 디렉토리나 사용자 패키지가 있는 작업 디렉토리를 지정하는데, 여러 개일 경우 ;(Semicolon)을 사용하여 연결한다. (Windows의 경우)
패키지 스코프(Package Scope)
패키지 내에는 함수, 구조체, 인터페이스, 메소드 등이 존재하는데, 이들의 이름(Identifier)이 첫 문자를 대문자로 시작하면 이는 public 으로 사용할 수 있다.
즉, 패키지 외부에서 이들을 호출하거나 사용할 수 있게 된다.
이름이 소문자로 시작하면 이는 non-public 으로 패키지 내부에서만 사용할 수 있다.
패키지 init 함수와 alias
개발자가 패키지를 작성할 때 패키지 실행 시 처음으로 호출되는 init() 함수를 작성할 수 있다.
init() 함수는 패키지가 로드 되면서 실행되는 함수로 별도의 호출 없이 자동으로 호출된다.
package testlib
var pop map[string]string
func init() { // 패키지 로드 시 map 초기화
pop = make(map[string]string)
}
경우에 따라 패키지를 import 하면서 단지 그 패키지 내의 init() 함수만을 호출하고자 하는 케이스가 있다.
이런 경우는 패키지 import 시 _(Underscore)라는 Alias를 지정한다.
아래의 예제는 other/xlib 패키지를 호출하면서 _ Alias를 지정한 예시이다.
package main
import _ "other/xlib"
만약 패키지 이름이 동일하지만, 서로 다른 버전 또는 서로 다른 위치에서 로딩 하고자 할 시, 패키지 Alias를 사용하여 구분할 수 있다.
import (
mongo "other/mongo/db"
mysql "other/mysql/db"
)
func main() {
mondb := mongo.Get()
mydb := mysql.Get()
}
사용자 정의 패키지 생성
개발자는 사용자 정의 패키지를 만들어서 재 사용 가능한 컴포넌트를 만들어 사용할 수 있다.
사용자 정의 라이브러리 패키지는 일반적으로 폴더를 하나 생성하고, 그 폴더 내에 .go 파일들을 만들어 구성한다.
하나의 서브 폴더 내에 있는 .go 파일들은 동일한 패키지 명을 기지며, 패키지 명은 해당 폴더의 이름과 같게 한다.
따라서 해당 폴더에 있는 여러 *.go 파일들은 하나의 패키지로 묶이게 된다.
아래의 예제는 간단한 패키지를 만드는 예제이다. /src 폴더 내에 (경로 C:\Program Files\Go\src) 'hello' 라는 폴더를 생성 후 다음의 코드를 입력한다. 입력 후 greeting.go 라는 파일로 저장한다.
패키지 명은 폴더 명인 'hello'와 동일해야 한다.
greeting.go
package hello
import "fmt"
func SayHello() {
fmt.Println("Hello, World!")
}
Optional : 사이즈가 큰 복잡한 라이브러리 같은 경우, 'go install' 명령어를 사용하여 라이브러리를 컴파일 하여 Cache할 수 있는데, 이렇게 하면 다음 빌드 시 빌드타임을 크게 줄일 수 있다.
작성이 완료되면 사용자 패키지를 사용하기 위해서 다음의 예제 코드를 작성해본다.
이때 main 패키지는 반드시 /src 내에 있을 필요는 없으며, 어디서든 작성한 패키지를 import 만 해주면 된다.
package main
import "hello"
func main() {
hello.SayHello()
}
Struct (구조체)
- Go에서 Struct는 Custom Data Type을 표현하는데 사용된다.
- Struct는 필드들의 집합체이며, 필드들의 컨테이너이다.
- struct는 필드 행위를 표현하는 데이터만을 가지며, 메소드를 갖지 않는다.
- Go언어는 객체지향 프로그래밍(Object Oriented Programming, OOP)을 고유의 방식으로 지원한다.
- Go에서는 전통적인 OOP 언어가 가지는 클래스, 객체, 상속 개념이 없다. 전통적인 OOP의 클래스(Class)는 Go언어에서Custom 타입을 정의하는 struct로 표현되는데, 전통적인 OOP의 클래스가 필드와 메소드를 함께 갖는 것과 달리 Go언어의 struct는 필드만을 가지며, 메소드는 별도로 분리하여 정의한다.
Struct 선언
아래의 예제는 type문을 사용하여 name과 age필드를 갖는 person이라는 struct를 정의한다.
만약 person 구조체를 패키지 외부에서 사용할 수 있게 하려면 struct명을 'Person' 으로 변경하면 된다.
package main
import "fmt"
// struct 정의
type person struct {
name string
age int
}
func main() {
// person 객체 생성
p := person{}
// 필드 값 설정
p.name = "Kim"
p.age = 20
fmt.Println(p)
}
struct 객체 생성
선언된 struct 타입으로부터 객체를 생성하는 방법은 몇 가지 방법들이 있다.
상단의 예제처럼 porson{} 를 사용하여 빈 person 객체를 먼저 할당 후, 나중에 그 필드 값을 채워 넣는 방법이 있다.
struct 필드에 접근하기 위해서는 .(Dot)을 사용한다.
struct 객체를 생성할 때, 초기 값을 함께 할당하는 방법도 있다.
아래의 예제는 struct 필드 값을 순서적으로 { }(Curly Brackets) 내에 넣는다.
var p person
p = person{"Kim", 20}
p1 = person{name : "Lee", age : 30}
순서에 상관없이 필드 명을 지정하고(Named Field) 그 값을 넣을 수도 있다.
필드 명을 지정할 시, 만약 일부 필드가 생략될 경우 생략된 필드들은 Zero Value 를 갖는다.
p := new(person)
p.name = "Kim" // p가 포인터라도 .(Dot)을 사용한다.
Go에서 struct는 기본적으로 변할 수 있는(Mutable) 개체로서 별도로 새 개체를 만들지 않고, 필드 값이 변화할 경우 해당 개체 메모리에서 직접 변경된다.
하지만 struct 개체를 다른 함수의 매개변수로 넘긴다면, pass by Value에 따라 객체를 복사해서 전달하게 된다.
그래서 만약 Pass by Reference로 struct를 전달하고자 한다면, struct의 포인터를 전달해야 한다.
생성자(Constructor) 함수
구조체(Struct)의 필드를 사용하기 전 초기화를 해야 하는 상황이 있을 수 있다.
예를 들어 struct의 필드가 map 타입인 경우 map을 사전에 미리 초기화 하면 외부 struct 사용자가 매번 map을 초기화를 하지 않아도 된다.
이러한 목적으로 생성자 함수를 사용할 수 있다.
생성자 함수는 struct를 반환하는 함수로써 그 함수 본문에서 필요한 필드를 초기화 한다.
아래의 예제에서 생성자 함수인 newDict()는 dict 라는 struct의 map 필드를 초기화한 후 그 struct 포인터를 반환하고 있다.
이어 main() 함수에서 struct 개체를 만들 때 dict 를 직접 생성하지 않고 대신 newDict() 함수를 호출하여 이미 초기화된 data 맵 필드를 사용하고 있다.
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"
}
메소드(Method)
- Go 언어는 객체지향 프로그래밍(OOP)을 고유의 방식으로 지원한다.
- 다른 언어의 OOP의 클래스가 필드와 메소드를 함께 갖는 것과 달리 GO언어에서는 struct가 필드만을 가지며, 메소드는 별도로 분리되어 정의된다.
- Go 메소드는 특별한 형태의 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 데이터를 복사하여 전달하며, 포인터 receiver는 struct의 포인터만을 전달한다.
Value receiver의 경우 만약 메소드 내에서 그 struct의 필드 값이 변경되더라도 호출자의 데이터는 변경되지 않는 반면, 포인터 receiver는 메소드 내의 필드 값 변경이 그대로 호출자에서 반영된다.
위의 예시에서 Rect.area() 메소드는 Value receiver를 표현한 것으로 만약 area() 메소드 내에서 width나 height가 변경되더라도 main() 함수의 rect 구조체의 필드 값에는 변화가 없다. 하지만, 아래의 예시와 같이 이를 포인터 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 출력
}
인터페이스(Interface)
구조체(struct)가 필드들의 집합체라고 하면 인터페이스(interface)는 메소드들의 집합체이다.
- 인터페이스란 추상 타입(Abstract Type)으로, 정의할 타입이 어떤 인터페이스의 인스턴스(Instance)가 되기 위해 반드시 구현해야 할 함수들을 정의한 것이다.이렇게 모든 메소드를 구현했다면 그 타입은 해당 인터페이스를 충족한다(satisfy)고 표현한다.
- 인터페이스의 가장 큰 장점은 함수의 매개변수 타입을 인터페이스로 정의했을 때 그 인터페이스를 구현한 변수라면어떤 것도 그 함수에 전달할 수 있다는 점이다.인터페이스는 특별한 기능을 직접 제공하지 않고 단지 형식을 맞추는 역할만 한다.
- Go 언어에서 인터페이스 타입을 정의할 때 구체적인 동작을 구현할 메소드의 집합을 나열하는 방식으로 표현한다.
- 어떤 타입이 특정한 인터페이스를 따르기 위해서는 그 인터페이스에서 정의한 모든 메소드를 구현해야 한다.
- 구조체(Struct)가 필드들의 집합체라면, 인터페이스는 메소드들의 집합체이다.
- 인터페이스는 타입(Type)이 구현해야 하는 메소드 원형(Prototype)들을 정의한다.
- 하나의 사용자 정의 타입이 인터페이스를 구현하기 위해서는 단순히 그 인터페이스가 갖는 모든 메소드들을 구현하면 된다.
인터페이스는 struct와 마찬가지로 type문을 사용하여 정의한다.
type Shape interface {
area() float64
perimeter() float64
}
인터페이스 구현
인터페이스를 구현하기 위해서는 해당 타입이 그 인터페이스의 메소드들을 모두 구현하면 되므로, 위의 Shape인터페이스를 구현하기 위해서는 area(), perimeter() 2개의 메소드만 구현하면 된다.
아래의 예제는 Rect와 Circle이라는 2개의 타입이 있을 때, Shape 인터페이스를 구현하기 위해서는 아래와 같이 각 타입별로 2개의 메소드를 구현해주면 된다.
// Rect 정의
type Rect struct {
width, height float64
}
// Circle 정의
type Circle struct {
radius float64
}
// Rect 타입에 대한 Shape 인터페이스 구현
func (r Rect) area() float64 {
return r.width * r.height
}
func (r Rect) perimeter() float64 {
return 2 * (r.width + r.height)
}
// Circle 타입에 대한 Shape 인터페이스 구현
func (c Circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
인터페이스 사용
인터페이스를 사용하는 일반적인 예로 함수가 매개변수로 인터페이스를 받아들이는 경우를 들 수 있다.
함수 매개변수가 interface인 경우, 이는 어떤 타입이든 해당 인터페이스를 구현하기만 하면 모두 입력 매개변수로 사용될 수 있다는 것을 의미한다.
아래의 예제에서 showArea() 함수는 Shape 인터페이스들을 매개변수로 받아들이고 있는데, Rect와 Circle 처럼 Shape 인터페이스를 구현한
타입 객체들을 매개변수로 받을 수 있다.
showArea() 함수 내에서 해당 인터페이스가 가진 메소드인 area() 또는 perimeter()를 사용할 수 있다.
func main() {
r := Rect {10., 20.}
c := Circle {10}
showArea(r, c)
}
func showArea(shapes ...Shape) {
for _, s := range shapes {
a := s.area() // 인터페이스 메소드 호출
println(a)
}
}
인터페이스 타입
Go 프로그래밍을 하다 보면 흔히 비어있는 인터페이스(Empty Interface)를 자주 접하게 되는데, 흔히 인터페이스 타입(Interface type)으로도 불린다.
아래의 예제에서 여러 표준 패키지들의 함수 Prototype을 살펴보면, 아래와 같이 빈 인터페이스가 자주 등장함을 볼 수 있다.
빈 인터페이스는 interface{}와 같이 표현한다.
func Marshal(v interface{}) ([]byte, error);
func Println(a ...interface{}) (n int, err error);
Empty interface는 메소드를 전혀 갖지 않는 빈 인터페이스로서, Go의 모든 Type은 적어도 0개의 메소드를 구현하므로, 흔히 Go에서
모든 Type을 나타내기 위해 빈 인터페이스를 사용한다.
빈 인터페이스는 어떠한 타입도 담을 수 있는 컨테이너라고 볼 수 있으며, 여러 다른 언어에서 흔히 일컫는 Dynamic Type 이라고 볼 수 있다.
아래의 예제에서 인터페이스 타입 x는 정수 1을 담았다가 다시 문자열 Kim을 담고 있는데, 실행 결과는 마지막에 담은 Kim을 출력한다.
package main
import "fmt"
func main() {
var x interface{}
x = 1
x = "Kim"
printIt(x)
}
func printIt(v interface{}) {
fmt.Println(v) // Kim
}
Type Assertion
Interface Type의 x와 타입 T에 대하여 x.(T)로 표현했을 때, 이는 x가 nil이 아니며, x는 T타입에 속한다는 점을 확인(Assert)하는 것으로
이러한 표현을 "Type Assertion"이라 부른다.
만약 x가 nil 이거나 x의 타입이 T가 아니라면, 런 타임 에러(Run Time Error)가 발생할 것이고, x가 T타입인 경우는 T타입의 x를 반환한다.
아래 예제에서 변수 j는 a.(int)로 부터 int형 변수 j가 된다.
func main() {
var a interface{} = 1
i := a // a와 i는 Dynamic Type, 값은 1
j := a.(int) // j는 int타입, 값은 1
println(i) // 포인터 주소 출력
println(j) // 1 출력
}
'Golang > Basic' 카테고리의 다른 글
Golang 헷갈리는 문법 복습 - 배열과 슬라이스 (0) | 2022.02.19 |
---|---|
Zero 부터 시작하는 Golang - 기본문법3 (0) | 2022.02.19 |
Zero 부터 시작하는 Golang - 기본문법1 (0) | 2022.02.13 |
Zero 부터 시작하는 Golang - 기본문법 (0) | 2022.02.13 |
Zero 부터 시작하는 Golang - 개요, 설치 (0) | 2022.02.08 |