Go언어의 접근 제어
Java에서는 default, public, protected, private와 같이 접근제어자가 존재한다. Go에서도 다른 패키지에서 import 해온 구조체, 함수, 변수 등에 대해 접근을 제어할 수 있는데 이름 첫 글자의 대/소문자로 판별한다. 예를 들어, 아래와 같이 accounts 패키지에 두 개의 구조체가 있고 구조체명 첫 글자의 대/소문자가 다르다고 할때, AccountA는 public 구조체로 접근이 가능하고 accountB는 private 구조체로 다른 패키지에서 접근이 불가능하다.
package accounts
type AccountA struct {
Owner string
Balance int
}
type accountB struct {
Owner string
Balance int
}
package main
func main() {
// 생성 가능 - public
account := accounts.AcccountA{Owner: "jisung", Balance: 1000}
// 생성 불가능 - private
// Cannot use the unexported type 'test' in the current package
account := accounts.accountB{owner: "jisung", balance: 1000}
}
이러한 접근 제어는 구조체 뿐만 아니라 Go에서 선언하는 모든 데이터에 적용된다. 아래와 같이 AccountA 구조체가 public이어도 내부 변수를 private으로 선언하면 외부 패키지에서 접근할 수 없다. Go의 구조체를 정리하는 글에서 접근제한 방식을 먼저 기술하는 이유는 다음과 같다. Go의 구조체에서도 Java와 같이 외부로부터 내부 구조를 캡슐화하고 외부에서의 데이터 조작을 컨트롤 해야한다. 또한, 생성자의 역할을 하는 함수와 Gettter/Setter의 역할을 하는 함수 등 외부로부터의 접근을 제어하는 방식을 사용하기 때문이다.
package accounts
type AccountA struct {
owner string
balance int
}
package main
func main() {
// 생성 불가능 - 내부 필드 private
// Unresolved reference 'owner', Unresolved reference 'balance'
account := accounts.AccountA(owner: "jisung", balance: 1000)
}
Constructor
Go의 구조체는 아쉽게도 Java의 new 또는 JS의 constructor 등과 같은 생성자라는 개념이 존재하지 않는다. 따라서, 구조체를 생성해 반환하는 함수를 만들어 생성자의 역할을 대신하게 해야한다. 아래 코드와 같이 같은 패키지 내에서는 private 필드에 접근이 가능하기 때문에 NewAccount에서 private 필드를 포함하는 구조체를 생성해 반환한다. 여기서 특이한 부분은 반환 타입에 포인터를 명시해주고 생성한 구조체의 주소를 반환하는 것이다. Go는 기본적으로 call-by-value이기 때문에 데이터를 전달할 때 데이터의 복사본을 전달한다. 그렇기 때문에 포인터를 사용해 생성한 구조체의 참조를 반환하고 메모리 비용을 줄이는 방식을 사용한다.
package accounts
type Account struct {
owner string
balance int
}
// NewAccount create Account
func NewAccount(owner string) *Account {
account := Account{owner: owner, balance: 0}
return &account
}
Receiver
Go의 구조체는 멤버 함수를 정의하는 것을 지원하지 않는다. 따라서, 구조체와 함수를 각각 선언해 사용해야 하는데 이때 구조체와 함수를 연결하고 함수로 구조체를 전달하는 역할을 해주는 것이 Receiver다. 아래 코드를 보면 Account 구조체와 Deposit 함수를 각각 선언했으나, Deposit 함수는 구조체 내부의 함수가 아니고 각각 독립적이기 때문에 Account를 통해 Deposit 함수를 호출할 수 없다. 물론 Deposit 함수에서 포인터를 이용해 매개변수로 Account를 받아오는 방법이 있지만, 그렇게 한다면 아주 복잡하고 이해하기 어려운 코드가 될 것이다. 모두가 좀 더 깔끔하고 직관적인 코드를 원하고 이러한 문제를 해결하기 위해 Receiver를 사용하는 것이라고 생각한다.
package accounts
type Account struct {
owner string
balance int
}
// NewAccount constructor
func NewAccount(owner string) *Account {
account := Account{owner: owner, balance: 0}
return &account
}
// Deposit x amount
func Deposit(amount int) {
a.balance += amount
}
package main
func main() {
account := accounts.NewAccount("jisung")
// error: Unresolved reference 'Deposit'
account.Deposit(1000)
}
Receiver의 사용 방법은 아래 코드와 같다. "func" 키워드와 메서드 이름 사이에 Receiver를 선언하면 구조체를 통해 함수를 호출할 수 있다. 주의해야할 점은 위에서도 언급했듯, Go는 기본적으로 call-by-value이다. 함수에 값을 전달할때 복사본을 전달하기 때문에 Receiver로 받아온 값의 수정이 필요하다면 Pointer Receiver를 사용해야 한다. 아래의 Deposit 함수와 같이 포인터를 이용해 참조값을 전달 받아 수정해야 실제 호출한 구조체의 값에 수정된 내용이 반영된다. Receiver의 네이밍 규칙은 일반적으로 구조체 이름 첫글자의 소문자를 사용한다.
package accounts
type Account struct {
owner string
balance int
}
// NewAccount constructor
func NewAccount(owner string) *Account {
account := Account{owner: owner, balance: 0}
return &account
}
// Deposit x amount
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// Balance of account
func (a Account) Balance() int {
return a.balance
}
package main
func main() {
account := accounts.NewAccount("jisung")
account.Deposit(1000)
fmt.Println(account)
}
//output:
//&{jisung 1000}
함수 예외처리
Go에는 다른 언어와 달리 try-catch를 이용한 예외처리 구문이 없고, throws와 같이 exception을 발생시킬 수 없다. 그렇기 때문에 unchecked excepion에 대해 코드레벨에서 모두 처리해줘야한다. 아래 코드의 Withdraw 함수와 같이 예외처리가 필요하다면 "errors.New()"를 이용해 직접 예외를 생성해야하고 함수에 error 타입의 반환을 명시해 예외를 전달한다.
package accounts
// Account struct
type Account struct {
owner string
balance int
}
var errNoMoney = errors.New("can't withdraw")
// NewAccount create Account
func NewAccount(owner string) *Account {
account := Account{owner: owner, balance: 0}
return &account
}
// Deposit x amount on your account
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// Withdraw x amount from your account
func (a *Account) Withdraw(amount int) error {
if a.balance < amount {
return errNoMoney
}
a.balance -= amount
return nil
}
// Balance of your account
func (a *Account) Balance() int {
return a.balance
}
package main
func main() {
account := accounts.NewAccount("jisung")
account.Deposit(1000)
err := account.Withdraw(500)
printErr(err)
err = account.Withdraw(1000)
printErr(err)
fmt.Println(account)
}
func printErr(err error) {
if err != nil {
fmt.Println(err)
}
}
//output:
//can't withdraw
//&{jisung 500}
함수 Override
Go에서도 Override가 가능하다. 구조체를 출력하는 부분을 예시로 들어보자. 기본적으로 구조체를 선언하고 출력하면 Go에서 미리 정의한 String() 함수가 실행되며 구조체 멤버의 값이 정해진 포맷으로 출력된다. 이때 String() 을 아래 코드와 같이 Override해서 사용할 수 있다.
package accounts
// Account struct
type Account struct {
owner string
balance int
}
// String print String
func (a *Account) String() string {
return fmt.Sprintf("%s's account.\nHas: %d", a.Owner(), a.Balance())
}
func main() {
account := accounts.NewAccount("jisung")
account.Deposit(1000)
fmt.Println(account)
}
//output - before override
//&{jisung 1000}
//output - after override
//jisung's account.
//Has: 1000
Map
지금까지 구조체를 이용해 Go에서 변수를 관리하고 함수를 정의해 사용하는 방법을 정리했다. Map도 아래 코드와 같이 구조체가 동작하는 방식와 다르지 않게 구성해 사용할 수 있다.
package mydict
import (
"errors"
"fmt"
)
// Dictionary type
type Dictionary map[string]string
var (
errNotFound = errors.New("not Found")
errCantUpdate = errors.New("can't update non-existing word")
errWordExists = errors.New("that word already exists")
)
//we don't need the * on the receiver because maps on Go are automatically using *
// Add a word to the dictionary
func (d Dictionary) Add(word string, def string) error {
_, err := d.Search(word)
switch {
case errors.Is(err, errNotFound):
fmt.Println("add success:", word)
d[word] = def
return nil
case err == nil:
return errWordExists
}
return nil
}
// Search for a word
func (d Dictionary) Search(word string) (string, error) {
value, exist := d[word]
if exist {
return value, nil
}
return "", errNotFound
}
// Update a word
func (d Dictionary) Update(word string, def string) error {
_, err := d.Search(word)
if errors.Is(err, errNotFound) {
return errCantUpdate
}
println("update success:", word)
d[word] = def
return nil
}
// Delete a word
func (d Dictionary) Delete(word string) {
delete(d, word)
}
Reference