1. 구조체 정의, 초기화, 그리고 메서드
구조체 정의와 초기화
Go 언어는 전통적인 '클래스' 키워드나 상속을 제공하지 않습니다. 대신 구조체(struct)를 내장(embedding)하고 인터페이스를 활용하여 더 유연하고 확장성 있는 객체지향 패턴을 구현합니다. 다음은 학생 정보를 담는 구조체 예시입니다.
type Student struct {
id uint
name string
male bool
score float64
}
Go에서는 생성자가 없으므로, 관례적으로 NewXXX 형태의 전역 함수를 생성자처럼 사용합니다.
func NewStudent(id uint, name string, male bool, score float64) *Student {
return &Student{id: id, name: name, male: male, score: score}
}
// 특정 필드만 초기화
func NewStudentV2(id uint, name string, score float64) *Student {
return &Student{id: id, name: name, score: score}
}
사용 예시:
s := NewStudent(1, "홍길동", true, 95.5)
fmt.Println(s)
값 리시버 메서드
메서드를 정의할 때는 함수명 앞에 리시버(receiver)를 선언합니다. 리시버는 메서드가 속한 타입을 지정합니다.
func (s Student) GetName() string {
return s.name
}
호출:
fmt.Println(s.GetName())
포인터 리시버 메서드
값을 변경해야 하는 메서드는 포인터 리시버를 사용합니다.
func (s *Student) SetName(name string) {
s.name = name
}
포인터 리시버는 구조체의 복사본이 아닌 원본을 수정합니다. 값 리시버는 복사본을 사용하므로 외부에 영향을 주지 않습니다.
s.SetName("김철수")
fmt.Println(s.GetName()) // "김철수"
String() 메서드 구현
Python의 __str__과 유사하게, Go는 String() 메서드로 기본 문자열 출력을 정의합니다.
func (s Student) String() string {
return fmt.Sprintf("{id: %d, name: %s, male: %t, score: %.1f}",
s.id, s.name, s.male, s.score)
}
fmt.Println(s) // 자동으로 String() 호출
2. 컴포지션을 통한 상속과 메서드 오버라이딩
Go는 상속 대신 컴포지션(composition)을 사용합니다. 구조체 안에 다른 구조체를 필드로 포함시켜 기능을 재사용합니다.
기본 구조체 정의
type Animal struct {
Name string
}
func (a Animal) Call() string {
return "동물의 울음소리..."
}
func (a Animal) FavorFood() string {
return "좋아하는 음식..."
}
func (a Animal) GetName() string {
return a.Name
}
컴포지션으로 상속 흉내내기
type Dog struct {
Animal // 임베딩
}
func main() {
animal := Animal{Name: "진돗개"}
dog := Dog{Animal: animal}
fmt.Println(dog.GetName()) // "진돗개"
fmt.Println(dog.Call()) // "동물의 울음소리..."
}
메서드 오버라이딩
func (d Dog) Call() string {
return "멍멍"
}
func (d Dog) FavorFood() string {
return "뼈다귀"
}
func main() {
dog := Dog{Animal: Animal{Name: "푸들"}}
fmt.Println(dog.Call()) // "멍멍"
fmt.Println(dog.Animal.Call()) // "동물의 울음소리..."
}
다중 임베딩과 충돌 해결
type Pet struct {
Name string
}
func (p Pet) GetName() string {
return p.Name
}
type Dog2 struct {
Animal
Pet
}
// dog2.GetName() // 컴파일 오류: ambiguous selector
fmt.Println(dog2.Animal.GetName()) // 명시적 호출 필요
포인터 임베딩
type Dog3 struct {
*Animal
Pet
}
func main() {
animal := Animal{Name: "시바견"}
pet := Pet{Name: "애완견"}
dog3 := Dog3{Animal: &animal, Pet: pet}
fmt.Println(dog3.GetName())
}
포인터 임베딩은 메모리 효율이 더 좋습니다.
임베딩 타입에 별칭 사용
type Dog4 struct {
animal *Animal
pet Pet
}
func main() {
animal := Animal{Name: "말티즈"}
pet := Pet{Name: "푸들"}
dog4 := Dog4{animal: &animal, pet: pet}
fmt.Println(dog4.animal.GetName())
}
3. 가시성(Visibility)
Go에서는 private, protected, public 같은 키워드가 없습니다. 대신 식별자(변수, 함수, 구조체 필드, 메서드 등)의 첫 글자가 대문자이면 패키지 외부에서 접근 가능하고, 소문자이면 패키지 내부에서만 접근 가능합니다.
패키지 예시 (animal 패키지):
animal.go
package animal
type Animal struct {
Name string // 대문자: 외부 접근 가능
}
func (a Animal) Call() string {
return "동물의 울음소리..." // 메서드도 동일 규칙
}
func (a Animal) FavorFood() string {
return "좋아하는 음식..."
}
func (a Animal) GetName() string {
return a.Name
}
pet.go
package animal
type Pet struct {
Name string
}
func (p Pet) GetName() string {
return p.Name
}
dog.go
package animal
type Dog struct {
Animal *Animal
Pet Pet
}
func (d Dog) FavorFood() string {
return "뼈다귀"
}
func (d Dog) Call() string {
return "멍멍"
}
main.go
package main
import (
"fmt"
. "example/animal" // '.' import로 패키지명 생략
)
func main() {
animal := Animal{Name: "진돗개"}
pet := Pet{Name: "애완견"}
dog := Dog{Animal: &animal, Pet: pet}
fmt.Println(dog.Animal.GetName())
fmt.Println(dog.Call())
}
비공개 속성을 통한 캡슐화
필드명을 소문자로 시작하면 비공개가 되어, 생성자를 통해서만 초기화할 수 있습니다.
animal.go (수정)
package animal
type Animal struct {
name string // 비공개
}
func NewAnimal(name string) Animal {
return Animal{name: name}
}
func (a Animal) GetName() string {
return a.name
}
main.go
func main() {
animal := NewAnimal("진돗개")
fmt.Println(animal.GetName()) // "진돗개"
// animal.name = "시바견" // 컴파일 오류
}
4. 인터페이스 정의와 구현
Go의 인터페이스는 타입 시스템의 핵심입니다. 클래스가 인터페이스를 명시적으로 implements하지 않아도, 해당 인터페이스가 요구하는 모든 메서드를 구현하고 있으면 자동으로 구현했다고 간주합니다(덕 타이핑).
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error) { return 0, nil }
func (f *File) Write(buf []byte) (n int, err error) { return 0, nil }
func (f *File) Seek(off int64, whence int) (pos int64, err error) { return 0, nil }
func (f *File) Close() error { return nil }
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
// File은 IFile, IReader 모두를 구현합니다.
인터페이스 임베딩
type A interface {
Foo()
}
type B interface {
A
Bar()
}
type T struct{}
func (t T) Foo() { fmt.Println("A.Foo") }
func (t T) Bar() { fmt.Println("B.Bar") }
// T는 B를 구현합니다. Foo만 구현하면 A만 구현합니다.
5. 인터페이스 할당
구체 타입 인스턴스를 인터페이스에 할당
type Integer int
func (a Integer) Add(b Integer) Integer { return a + b }
func (a Integer) Multiply(b Integer) Integer { return a * b }
type Math interface {
Add(i Integer) Integer
Multiply(i Integer) Integer
}
var a Integer = 10
var m Math = a // 값 리시버일 때 가능
m2 := Math(&a) // 포인터도 가능
fmt.Println(m.Add(5)) // 15
포인터 리시버가 포함된 경우
func (a *Integer) Add(b Integer) {
*a = *a + b
}
// var m Math = a // 컴파일 오류! Integer는 Add를 값 리시버로 갖고 있지 않음
var m Math = &a // 포인터만 가능
인터페이스 간 할당
메서드 집합이 같은 인터페이스는 서로 할당 가능합니다.
type Number1 interface {
Equal(i int) bool
LessThan(i int) bool
MoreThan(i int) bool
}
type Number2 interface {
Equal(i int) bool
MoreThan(i int) bool
LessThan(i int) bool // 순서 달라도 동일
}
type Number int
func (n Number) Equal(i int) bool { return int(n) == i }
func (n Number) LessThan(i int) bool { return int(n) < i }
func (n Number) MoreThan(i int) bool { return int(n) > i }
var num Number = 5
var n1 Number1 = num
var n2 Number2 = n1 // 가능
메서드 집합이 부분집합인 경우에도 할당 가능하지만, 역은 불가능합니다.
// Number2에 Add 메서드가 더 있다고 가정
// var n1 Number1 = n2 // 컴파일 오류
6. 타입 단언(Type Assertion)
인터페이스 타입 단언
var num Number = 10
var n2 Number2 = &num
// n2가 Number1도 구현하는지 확인
if n1, ok := n2.(Number1); ok {
fmt.Println(n1.Equal(10)) // true
}
구조체 타입 단언
type IAnimal interface {
GetName() string
Call() string
FavorFood() string
}
var animal = NewAnimal("진돗개")
var ianimal IAnimal = Dog{Animal: animal}
if dog, ok := ianimal.(Dog); ok {
fmt.Println(dog.Call()) // "멍멍"
}
주의: Go의 컴포지션은 상속이 아니므로, 부모 타입으로 단언하면 실패합니다.
// ianimal.(Animal) // false
리플렉션 기반 타입 단언
import "reflect"
func checkType(args ...interface{}) {
for _, arg := range args {
switch reflect.TypeOf(arg).Kind() {
case reflect.Int:
fmt.Println(arg, "is int")
case reflect.String:
fmt.Println(arg, "is string")
}
}
}
간단한 타입은 .(type) 스위치로도 가능합니다.
func whatType(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("unknown")
}
}
7. 빈 인터페이스, 리플렉션, 그리고 제네릭
빈 인터페이스 (interface{})
모든 타입을 나타낼 수 있습니다. Java의 Object와 유사하지만 더 간결합니다.
var v1 interface{} = 42
var v2 interface{} = "hello"
var v3 interface{} = struct{ Name string }{"Go"}
함수 인자로도 사용됩니다.
func PrintAnything(v interface{}) {
fmt.Println(v)
}
리플렉션(Reflection)
reflect 패키지를 사용하면 런타임에 타입 정보를 얻고 조작할 수 있습니다.
type MyStruct struct {
Field1 string
Field2 int
}
func main() {
s := MyStruct{"hello", 42}
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
fmt.Println("Type:", t.Name()) // "MyStruct"
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field %d: %s (%s) = %v\n", i, field.Name, field.Type, v.Field(i))
}
}
리플렉션 기반 제네릭 컨테이너
type Container struct {
slice reflect.Value
}
func NewContainer(t reflect.Type, size int) *Container {
if size <= 0 {
size = 64
}
return &Container{
slice: reflect.MakeSlice(reflect.SliceOf(t), 0, size),
}
}
func (c *Container) Put(val interface{}) error {
if reflect.ValueOf(val).Type() != c.slice.Type().Elem() {
return fmt.Errorf("type mismatch: expected %s, got %T", c.slice.Type().Elem(), val)
}
c.slice = reflect.Append(c.slice, reflect.ValueOf(val))
return nil
}
func (c *Container) Get(val interface{}) error {
if reflect.ValueOf(val).Kind() != reflect.Ptr ||
reflect.ValueOf(val).Elem().Type() != c.slice.Type().Elem() {
return fmt.Errorf("invalid get target")
}
reflect.ValueOf(val).Elem().Set(c.slice.Index(0))
c.slice = c.slice.Slice(1, c.slice.Len())
return nil
}
func main() {
c := NewContainer(reflect.TypeOf(0), 16)
for _, v := range []int{1, 2, 3} {
c.Put(v)
}
var result int
c.Get(&result)
fmt.Println(result) // 1
}
빈 구조체(struct{})
struct{}는 메모리를 전혀 차지하지 않습니다. 주로 채널 시그널링에 사용됩니다.
ch := make(chan struct{})
go func() {
// 작업 수행
ch <- struct{}{} // 빈 값 전송
}()
<-ch // 완료 신호 수신