이 글은 Must Have Tucker의 Go 언어 프로그래밍 책을 참고하여 작성하였으며, 개인적인 학습 내용을 정리한 글입니다.
패키지 란?
Go에서 패키지는 코드를 묶는 가장 큰 단위 입니다.
패키지 = 관련된 Go 파일들의 집합
- 하나의 패키지는 하나의 디렉터리
- 같은 디렉터리에 있는 .go 파일들은 모두 같은 패키지명을 가져야 합니다.
- 코드 재사용, 의존성 관리, 네임스페이스 역할
myapp/
├─ main.go ← package main
├─ utils/
│ ├─ math.go ← package utils
│ └─ string.go ← package utilsutils 디렉터리에 있는 .go 파일들은 모두 package utils 로 같은 패키지를 사용하는데,
main.go 는 package myapp이 아니라 package main 으로 시작을 해야 합니다.
왜 이런 것인지 알아보겠습니다.
main 패키지
main 패키지는 특별한 패키지로 프로그램 시작점을 포함한 패키지 입니다.
프로그램 시작점이란 main() 함수를 의미 합니다.
프로그램이 실행되면 운영체제는 프로그램을 메모리로 올립니다. 그리고 이것을 로드(Load) 라고 합니다.
그런 다음 프로그램 시작점 부터 한 줄씩 코드를 실행합니다. 이 때 프로그램 시작점이 main() 함수이고,
main() 함수를 포함한 패키지가 main 패키지 입니다.
이제 패키지 사용법에 대해 알아보겠습니다.
패키지 사용하기
패키지를 사용하려면 import 예약어로 임포트를 하고 원하는 패키지 경로를 따옴표로 묶어서 사용해야합니다.
import "fmt"여러 패키지를 임포트 하려면 아래와 같이 하면 됩니다.
import (
"fmt"
"os"
)패키지 멤버에 접근하기
패키지를 사용하려고 import로 가져오면 해당 패키지에 접근을 하려면 점(.) 연산자를 사용해 패키지에서 제공하는 함수, 구조체 등에 접근할 수 있습니다.
예를 들어,
fmt.Println("Hello world")이렇게 fmt 패키지에 . 연산자를 사용해서 Println() 함수를 사용하는 것 처럼 다른 패키지들도 이와 동일한 방식으로 사용 하면 됩니다.
경로(Path)가 있는 패키지 사용하기
예시 코드로 바로 알아보자면,
//ch14/ex14.1/ex14.1.go
package main
import ( // ❶ 둘 이상의 패키지는 소괄호로 묶어줍니다.
"fmt"
"math/rand" // ❷ 패키지명은 rand입니다.
)
func main() {
fmt.Println(rand.Int()) // ❸ 랜덤한 숫자값을 출력합니다.
}❷ fmt 패키지와 달리 math/rand 라는 패키지를 사용하려고 import에 정의를 해줍니다.
❸ 실제로 패키지를 사용하려면, 패키지의 가장 끝 경로에 있는 폴더명을 정의해주면 됩니다. 여기서는 rand 가 됩니다.
따라서 마지막에 fmt.Println(rand.Int()) 이렇게 사용한 것을 알 수 있습니다.
겹치는 패키지 문제, 별칭으로 풀기
만약 패키지명이 겹칠 땐 어떻게 할까요?
import (
"text/template"
"html/template"
)위 예시 처럼 패키지의 끝 폴더명이 template 일 때는 어떻게 사용을 하냐?
별칭을 붙여줘서 해결하면 됩니다.
import (
"text/template"
htemplate "html/template" // 별칭 htemplate
)
func main() {
template.New("foo").Parse(`{{define "T"}}Hello`)
htemplate.New("foo").Parse(`{{define "T"}}Hello`)
}사용하지 않는 패키지 포함하기
Go 언어에서는 사용하지 않는 패키지를 import에 정의하고 실행시키면 에러가 발생합니다.
패키지를 직접 사용하지 않지만 부가효과를 얻고자 import에 정의하는 경우 밑줄_ 을 패키지명 앞에 붙여주면 됩니다.
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // 밑줄 _을 이용해서 오류 방지
)Go 모듈
Go 모듈은 Go 패키지들을 모아놓은 Go 프로젝트 단위 입니다.
즉, Go 모듈 = 이 프로젝트의 이름표 + 의존성 잠금 장치
Go 1.16 버전부터는 Go 모듈 사용이 기본이 되어 모든 Go 코드는 Go 모듈 아래 있어야 합니다.
실제로 Go 모듈은 go.mod 파일로 정의 됩니다.
myapp/
├─ go.mod ← 이게 모듈
├─ main.go
└─ utils/그래서 go.mod 파일은 모듈 이름과 Go 버전, 필요한 외부 패키지 등이 명시되어 있습니다.
Go 모듈은 go mod init 명령을 통해 만들 수 있습니다.
go mod init [패키지명]패키지명과 패키지 외부 공개
Go 언어에서 패키지명을 지을 때 아래와 같이 권장 합니다.
- 쉽고 간단하게 지을 것
- 모든 문자를 소문자로 할 것
그리고 이전에도 언급했듯이, 패키지 전역으로 선언된 첫 글자가 대문자로 시작되는 모든 변수, 상수, 타입, 함수, 메서드는 패키지 외부로 공개 됩니다.
이제 패키지 외부로 공개되는 것과 공개되지 않는 것을 예제로 알아보겠습니다.
- 상수, 변수, 구조체, 함수 등을 정의 해놓은 publicpkg 패키지 정의
package publicpkg
import "fmt"
const (
PI = 3.1415 // 외부로 공개되는 상수
pi = 3.141516 // 외부로 공개되지 않는 상수
)
var ScreenSize int = 1080 // 외부로 공개되는 변수
var screenHeight int // 외부로 공개되지 않는 변수
func PublicFunc() { // 외부로 공개되는 함수
const MyConst = 100 // 외부로 공개되지 않습니다.
fmt.Println("This is a public function", MyConst)
}
func privateFunc() { // 외부로 공개되지 않는 함수
fmt.Println("This is a private function")
}
type MyInt int // 외부로 공개되는 별칭 타입
type myString string // 외부로 공개되지 않는 별칭 타입
type MyStruct struct { // 외부로 공개되는 구조체
Age int // 외부로 공개되는 구조체 필드
name string // 외부로 공개되지 않는 구조체 필드
}
func (m MyStruct) PublicMethod() { // 외부로 공개되는 메서드
fmt.Println("This is a public method")
}
func (m MyStruct) privateMethod() { // 외부로 공개되지 않는 메서드
fmt.Println("This is a private method")
}
type myPrivateStruct struct { // 외부로 공개되지 않는 구조체
Age int // 외부로 공개되지 않는 구조체 필드
name string // 외부로 공개되지 않는 구조체 필드
}
func (m myPrivateStruct) PrivateMethod() { // 외부로 공개되지 않는 메서드
fmt.Println("This is a private method")
}위 코드로 publicpkg 패키지를 정의했습니다.
이제 아래 코드를 생성 후 실행시켜보고 결과 값을 확인해보면,
- 실제로 외부 노출이 되는 지 확인하기 위한 코드
package main
import (
"fmt"
"ch14/ex14.2/publicpkg"
)
func main() {
fmt.Println("PI:", publicpkg.PI)
publicpkg.PublicFunc()
var myint publicpkg.MyInt = 10
fmt.Println("myint:", myint)
var mystruct = publicpkg.MyStruct{Age: 18}
fmt.Println("mystruct:", mystruct)
}
---
PI: 3.1415
This is a public function 100
myint: 10
mystruct: {18 }publicpkg 패키지에서 외부로 노출 가능하도록 설정해둔 상수, 함수, 구조체 등을 확인할 수 있습니다.
그런데 헷갈리는 게, 어떤 건 외부 노출이 되고 어떤 건 외부 노출이 안돼고 이러한 내용이 있어서 표로 간단하게 정리해보았습니다.
| 식별자 | 종류 | 선언 위치/소속 | 패키지 외부 접근 | 왜? |
|---|---|---|---|---|
| PI | 상수 | package scope | ✅ 가능 | 대문자로 시작하는 패키지 레벨 식별자(exported) |
| pi | 상수 | package scope | ❌ 불가 | 소문자로 시작하는 패키지 레벨 식별자(unexported) |
| ScreenSize | 변수 | package scope | ✅ 가능 | 대문자로 시작하는 패키지 레벨 식별자(exported) |
| screenHeight | 변수 | package scope | ❌ 불가 | 소문자로 시작하는 패키지 레벨 식별자(unexported) |
| PublicFunc | 함수 | package scope | ✅ 가능 | 대문자로 시작하는 패키지 레벨 함수(exported) |
| MyConst | 상수 | function scope (PublicFunc 내부) | ❌ 불가 | 함수(블록) 스코프라 외부 접근 경로가 없음(=export 개념 적용 대상 아님) |
| privateFunc | 함수 | package scope | ❌ 불가 | 소문자로 시작하는 패키지 레벨 함수(unexported) |
| MyInt | 타입(alias/defined type) | package scope | ✅ 가능 | 대문자로 시작하는 패키지 레벨 타입(exported) |
| myString | 타입(alias/defined type) | package scope | ❌ 불가 | 소문자로 시작하는 패키지 레벨 타입(unexported) |
| MyStruct | 구조체 타입 | package scope | ✅ 가능 | 대문자로 시작하는 패키지 레벨 타입(exported) |
| MyStruct.Age | 구조체 필드 | 타입(MyStruct) 멤버 | ✅ 가능 | 필드명이 대문자이고, 소속 타입(MyStruct)도 exported라 접근 경로가 열림 |
| MyStruct.name | 구조체 필드 | 타입(MyStruct) 멤버 | ❌ 불가 | 필드명이 소문자라 unexported(외부에서 필드 선택 불가) |
| (MyStruct) PublicMethod | 메서드 | 수신자: MyStruct(exported) | ✅ 가능 | 메서드명 대문자 + 수신자 타입(MyStruct)가 exported라 외부에서 호출 가능 |
| (MyStruct) privateMethod | 메서드 | 수신자: MyStruct(exported) | ❌ 불가 | 메서드명이 소문자라 unexported |
| myPrivateStruct | 구조체 타입 | package scope | ❌ 불가 | 타입명이 소문자라 unexported(외부에서 타입 자체를 참조 불가) |
| myPrivateStruct.Age | 구조체 필드 | 타입(myPrivateStruct) 멤버 | ❌ 불가 | 필드명이 대문자여도, 소속 타입가 unexported라 외부에서 그 타입 경로로 접근 자체가 불가 |
| myPrivateStruct.name | 구조체 필드 | 타입(myPrivateStruct) 멤버 | ❌ 불가 | 필드명도 소문자 + 타입도 unexported |
| (myPrivateStruct) PrivateMethod | 메서드 | 수신자: myPrivateStruct(unexported) | ❌ 불가 | 메서드명은 대문자여도 수신자 타입이 unexported라 외부에서 값/변수 생성·참조가 불가 → 호출 경로가 막힘 |
패키지 초기화
패키지를 임포트하고 나면, 아래와 같은 순서로 동작을 합니다.
- 패키지를 임포트하면 컴파일러는 패키지 내 전역 변수를 초기화 합니다.
- 그런 다음 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 합니다. 여기서 init() 함수는 반드시 입력 매개변수가 없고 반환값도 없는 함수여야 합니다.
그런데 만약 어떤 패키지의 초기화 함수인 init() 함수 기능만 사용하길 원할 경우 밑줄 _을 이용해서 임포트 합니다.
ex)
import
"database/sql"
_ "github.com/mattn/go-splite3" // 이렇게 밑줄 _ 을 이용해서 init() 함수 호출이제 예시를 통해 자세히 알아보겠습니다.
- exinit 패키지 정의
package exinit
import "fmt"
var (
a = c + b //
b = f() //
c = f() //
d = 3 //
)
func init() {
d++
fmt.Println("init function", d)
}
func f() int { //
d++ //
fmt.Println("f() d:", d) //
return d //
}
func PrintD() {
fmt.Println("d:", d)
}만약 exinit 패키지를 다른 코드에서 import 해서 사용을 하면 앞서 이야기 했던 내용 대로
가장 먼저, 패키지 내 전역 변수들을 초기화 시켜줍니다.
그래서
- a값은 c, b가 초기화 된 다음 초기화 되고
- b 값은 f() 함수에서 초깃값이 d=3으로 정의 되어있는 걸로 초기화 되어 4가 되고
- c 값은 b 값을 초기화할 때, d=4 로 초기화되어 c 값은 5가 되고
- d 값은 c 값을 초기화할 때, d=5 로 초기화 되어 마지막으로 d 값은 6이 됩니다.
이렇게 전역 변수들의 값 초기화가 먼저 되고 그 이후 init() 함수가 실행된 다음에 exinit 패키지를 사용할 수 있게 됩니다.
그래서 아래 코드에서 결과를 보면,
- exinit 패키지를 사용하는 코드
package main
import (
"ch14/ex14.3/exinit" // exinit 패키지 임포트
"fmt"
)
func main() {
fmt.Println("main function")
exinit.PrintD()
}
---
f() d: 4
f() d: 5
init function 6
main function
d: 6결과 값의 내용을 보면,
방금 전에 말한 것 처럼 exinit 패키지를 import 한 시점에 전역 변수들의 값이 제일 먼저 초기화가 이루어지고,
main() 함수가 실행되는 것을 확인할 수 있습니다.
즉, 간단하게 요약을 하면 패키지를 import 하면 패키지 초기화가 시작되는데 이때 패키지 내 모든 전역 변수들이 초기화 되고 그 다음에 init() 함수가 호출 되고 난 이후에 main() 함수가 시작 됩니다.
이상으로 패키지에 대해 알아보았습니다.
감사합니다.
'개발 > Go' 카테고리의 다른 글
| [Go 언어 학습기 #9] Go 언어(Golang) 의 포인터 (0) | 2025.12.24 |
|---|---|
| [Go 언어 학습기 #8] Go 언어(Golang) 의 슬라이스 (0) | 2025.12.23 |
| [Go 언어 학습기 #6] Go 언어(Golang) 의 구조체 (0) | 2025.12.18 |
| [Go 언어 학습기 #5] Go 언어(Golang) 의 배열 (0) | 2025.12.17 |
| [Go 언어 학습기 #4] Go 언어(Golang) 의 함수 (0) | 2025.12.02 |
