본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 20장~21장

  • 20장 인터페이스
  • 21장 함수고급편

 

20장 인터페이스

 

 

  • 인터페이스의 구조는 아래소스와 같이 type 인터페이스명 interface { } 로 선언한다.
  • type 키워드로 선언하기때문에 변수선언이 가능하다.
  • 아래의 소스에서 Stringer라는 메서드를 만들었다. Student를 생성하여 , 해당 student를 stringer에 대입하여, stringer의 String()메서드를 호출하는 예제이다.

 

//ch20/ex20.1/ex20.1.go
package main

import "fmt"

type Stringer interface { // ❶ Stringer 인터페이스 선언
	String() string
}

type Student struct {
	Name string
	Age  int
}

func (s Student) String() string { // ❷ Student의 String() 메서드

	return fmt.Sprintf("안녕! 나는 %d살 %s라고 해", s.Age, s.Name) // ❸ 문자열 만들기
}

func main() {
	student := Student{"철수", 12} // Student 타입
	var stringer Stringer        // Stringer 타입

	stringer = student // 4 stringer값으로 student 대입

	fmt.Printf("%s\n", stringer.String()) // 5 stringer의 String() 메서드 호출
}

 

 

 

  • 아래 소스는 Fedex에서 제공한 패키지를 사용하여, Fedex전송객체를 만들어 SendBook()을 호출하는 프로그램이다.
//ch20/ex20.2/ex20.2.go
package main

import "github.com/tuckersGo/musthaveGo/ch20/fedex"

func SendBook(name string, sender *fedex.FedexSender) {
	sender.Send(name)
}

func main() {
	// Fedex 전송 객체를 만듭니다.
	sender := &fedex.FedexSender{}
	SendBook("어린 왕자", sender)
	SendBook("그리스인 조르바", sender)
}

 

 

//ch20/fedex/fedex.go
// Fedex에서 제공한 패키지입니다.
package fedex

import "fmt"

// Fedex에서 제공한 패키지 내 전송을 담당하는 구조체입니다.
type FedexSender struct {
}

func (f *FedexSender) Send(parcel string) {
	fmt.Printf("Fedex sends %v parcel\n", parcel)
}

 

 

 

  • Fedex에서 제공한 fedex패키지를 사용하지 많고, 이번에는 우체국에서 제공한 패키지를 사용해보자. 그러면 아래와 같이 타입이 맞지 않아 오류가 발생한다.
//ch20/ex20.3/ex20.3.go
package main

import (
	"github.com/tuckersGo/musthaveGo/ch20/fedex"
	"github.com/tuckersGo/musthaveGo/ch20/koreaPost"
)

func SendBook(name string, sender *fedex.FedexSender) {
	sender.Send(name)
}

func main() {
	// 우체국 전송 객체를 만듭니다.
	sender := &koreaPost.PostSender{} // ❶ *koreaPost.PostSender 타입
	SendBook("어린 왕자", sender)         // ❷ 타입이 맞지 않습니다.
	SendBook("그리스인 조르바", sender)
}

 

 

//ch20/koreaPost/post.go
// 우체국에서 제공한 패키지입니다.
package koreaPost

import "fmt"

// 우체국에서 제공한 패키지 내 전송을 담당하는 구조체입니다.
type PostSender struct {
}

func (k *PostSender) Send(parcel string) {
	fmt.Printf("우체국에서 택배 %v를 보냅니다.\n", parcel)
}

 

 

 

 

  • 아래와 같이 Sender인터페이스를 사용하여, Fedex, KoreaPost둘다 사용이 가능하다.
  • 둘다 Send메서드가 있어서, 인터페이스를 통해서 각각의 Send()메서드가 호출된다. 
//ch20/ex20.4/ex20.4.go
package main

import (
	"github.com/tuckersGo/musthaveGo/ch20/fedex"
	"github.com/tuckersGo/musthaveGo/ch20/koreaPost"
)

// ❶ Sender 인터페이스를 만들었습니다.
type Sender interface {
	Send(parcel string)
}

// ❷ Sender 인터페이스를 입력으로 받습니다.
func SendBook(name string, sender Sender) {
	sender.Send(name)
}

func main() {
	// ❸ 우체국 전송 객체, Fedex 전송 객체 모두 SendBook 인수로 사용할 수 있습니다.
	// 우체국 전송 객체를 만듭니다.
	koreaPostSender := &koreaPost.PostSender{}
	SendBook("어린 왕자", koreaPostSender)
	SendBook("그리스인 조르바", koreaPostSender)

	// Fedex 전송 객체를 만듭니다.
	fedexSender := &fedex.FedexSender{}
	SendBook("어린 왕자", fedexSender)
	SendBook("그리스인 조르바", fedexSender)
}

 

 

 

  • 아래 소스와 같이  빈 인터페이스(v interface{})를 인수로 받을수있다.
  • v.(type) 을 사용하여 해당 타입에 맞는 로직을 수행할 수 있다.
//ch20/ex20.5/ex20.5.go
package main

import "fmt"

func PrintVal(v interface{}) { // ❶ 빈 인터페이스를 인수로 받는 함수
	switch t := v.(type) {
	case int:
		fmt.Printf("v is int %d\n", int(t))
	case float64:
		fmt.Printf("v is float64 %f\n", float64(t))
	case string:
		fmt.Printf("v is string %s\n", string(t))
	default:
		// 그외 타입인 경우 타입과 값을 출력합니다.
		fmt.Printf("Not supported type: %T:%v\n", t, t)
	}
}

type Student struct {
	Age int
}

func main() {
	PrintVal(10)          // int
	PrintVal(3.14)        // float64
	PrintVal("Hello")     // string
	PrintVal(Student{15}) // Student
}

 

 

 

  • 인터페이스 변수의 기본값은 nil값이기때문에 바로 사용하면 오류(런타임에러)가 발생한다. 
//ch20/ex20.6/ex20.6.go
package main

type Attacker interface {
	Attack()
}

func main() {
	var att Attacker // ❶ 기본값은 nil입니다.
	att.Attack()     // ❷ att가 nil이기 때문에 런타임 에러가 발생합니다.
}

 

 

  • 아래의 소스에서는 Stringer 인터페이스, Student 구조체, Student가 리시버인 String()메서드가 있다. 
  • main에서 PrintAge로 Student인 s를 호출하면, PrintAge에서는 Stringer리시버로 받는다.
  • stringer 리시버를 Student로 타입변환하여 , Student의 Age를 출력하는 예제이다. 
//ch20/ex20.7/ex20.7.go
package main

import "fmt"

type Stringer interface { // ❶ 인터페이스
	String() string
}

type Student struct { // ❷ 구조체
	Age int
}

func (s *Student) String() string { // ❸ Student 타입의 String() 메서드
	return fmt.Sprintf("Student Age:%d", s.Age)
}

func PrintAge(stringer Stringer) { // ➍

	s := stringer.(*Student)       // ➎ *Student 타입으로 타입 변환
	fmt.Printf("Age: %d\n", s.Age) // ➏ s.Age 출력

}

func main() {
	s := &Student{15} // ➐ *Student 타입 변수 s 선언 및 초기화

	PrintAge(s) // ➑ 변수 s 를 인터페이스 인수로 PrintAge() 함수 호출
}

 

 

  • 아래의 경우에는 Student구조체가 String() 메서드를 포함하고 있지 않기때문에 컴파일 오류가 발생한다. 
//ch20/ex20.8/ex20.8.go
package main

type Stringer interface {
	String() string
}

type Student struct {
}

func main() {
	var stringer Stringer
	stringer.(*Student) // ❶ 빌드 타임 에러 발생
}

 

 

  • 아래의 소스에서는 Actor, Student 둘다 String()메서드가 있지만, actor를 인자로 받아 Student로 변경할수없다. 
//ch20/ex20.9/ex20.9.go
package main

import "fmt"

type Stringer interface {
	String() string
}

type Student struct {
}

func (s *Student) String() string {
	return "Student"
}

type Actor struct {
}

func (a *Actor) String() string {
	return "Actor"
}

func ConvertType(stringer Stringer) {
	// ❷ 런타임 에러 발생: *Student 타입은 Stringer 인터페이스로 쓰일 수 있지만
	// stringer값이 *Student 타입이 아니기 때문에 에러가 발생합니다.
	student := stringer.(*Student)
	fmt.Println(student)
}

func main() {
	// ❶ *Actor 구조체 값을 ConvertType() 함수의 인수로 사용합니다.
	actor := &Actor{}
	ConvertType(actor)
}

 

 

 

 

  • File은 Read()를 메서드로 갖는 구조체이다. ReadFile에서 Closer로 타입변환하면 오류가 발생한다. 
//ch20/ex20.10/ex20.10.go
package main

type Reader interface {
	Read()
}

type Closer interface {
	Close()
}

type File struct {
}

func (f *File) Read() {
}

func ReadFile(reader Reader) {
	// ❷ Reader 인터페이스 변수를 Closer 인터페이스로 타입 변환합니다.
	// 런타임 에러가 발생합니다.
	c := reader.(Closer)
	c.Close()
}

func main() {
	// ❶ File 포인터 인스턴스를 ReadFile() 함수의 인수로 사용합니다.
	file := &File{}
	ReadFile(file)
}

 

 

 

 

 

 

 

 

 

 

21장 함수고급편

 

//ch21/ex21.1/ex21.1.go
package main

import "fmt"

func sum(nums ...int) int { // ❶ 가변 인수를 받는 함수
	sum := 0

	fmt.Printf("nums 타입: %T\n", nums) // // ❷ nums 타입 출력
	for _, v := range nums {
		sum += v
	}
	return sum
}

func main() {
	fmt.Println(sum(1, 2, 3, 4, 5)) // 5개의 인수를 사용합니다.
	fmt.Println(sum(10, 20))        // 2개의 인수를 사용합니다.
	fmt.Println(sum())              // 0개의 인수를 사용합니다.
}

 

 

 

  • defer 에 대한 예제이다. 이 명령역시 7장에서 설명하였다.
//ch21/ex21.2/ex21.2.go
package main

import (
	"fmt"
	"os"
)

func main() {

	f, err := os.Create("test.txt") // ❶ 파일 생성
	if err != nil {                 // ❷ 에러 확인
		fmt.Println("Failed to create a file")
		return
	}

	defer fmt.Println("반드시 호출됩니다.") // ❸ 지연 수행될 코드
	defer f.Close()                 // ➍ 지연 수행될 코드
	defer fmt.Println("파일을 닫았습니다")  // ➎ 지연 수행될 코드

	fmt.Println("파일에 Hello World를 씁니다.")
	fmt.Fprintln(f, "Hello World") // ➏ 파일에 텍스트를 씁니다.
}

 

 

 

 

함수타입변수

  • getOperator의 return 타입은 func(int, int) int 의 함수타입이다. 
  • 해당 함수의 리턴값은 함수를 리턴한다. 
  • 예제에서는 getOperator("*")를 호출하는데 * 이므로 mul() 함수를 반환하여, operator() 는 mul()과 같다.
//ch21/ex21.3/ex21.3.go
package main

import "fmt"

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

func mul(a, b int) int {
	return a * b
}

func getOperator(op string) func(int, int) int { // ❶ op에 따른 함수 타입 반환
	if op == "+" {
		return add
	} else if op == "*" {
		return mul
	} else { // ❷ +나 *가 아니면 nil 반환
		return nil
	}
}

func main() {
	// ❸ int 타입 인수 2개를 받아서 int 타입 반환을 하는 함수 타입 변수
	var operator func(int, int) int
	operator = getOperator("*")

	var result = operator(3, 4) // ❹ 함수 타입 변수를 사용해서 함수 호출
	fmt.Println(result)
}

 

 

함수리터럴

 

  • 아래소스는 위의소스와 같은 기능을 하는데, return값에 익명함수를 바로 적는다. 함수이름이 없기때문에 익명함수 또는 람다라고 하는데, Go에서는 함수리터럴이라고 부른다. 
//ch21/ex21.4/ex21.4.go
package main

import "fmt"

type opFunc func(a, b int) int

func getOperator(op string) opFunc {
	if op == "+" {

		return func(a, b int) int { // ❶ 함수 리터럴을 사용해서 더하기 함수를 정의하고 반환
			return a + b
		}
	} else if op == "*" {

		return func(a, b int) int { // ❷ 함수 리터럴을 사용해서 곱하기 함수를 정의하고 반환
			return a * b
		}
	} else {
		return nil
	}
}

func main() {
	fn := getOperator("*")

	result := fn(3, 4) // ❸ 함수 타입 변수를 사용해서 함수 호출
	fmt.Println(result)
}

 

 

  • 아래의 경우에 결과값은 11이 된다. 함수리터럴은 외부변수의 값을 바꿀수있다. 
//ch21/ex21.5//ex21.5.go
package main

import "fmt"

func main() {
	i := 0

	f := func() {
		i += 10 // ❶ i에 10 더하기
	}

	i++

	f() // ❷ f 함수 타입 변수를 사용해서 함수 리터럴 실행

	fmt.Println(i)
}

 

 

 

  • 함수리터럴은 외부변수를 내부상태로 가져온다.(Capture) 캡쳐는 값복사가 아닌 참조형태로 가져오기때문에 주의가 필요하다.
  • 아래는 캡쳐를 하기때문에 발생할수있는 문제에 대한 에제이다.
//ch21/ex21.6/ex21.6.go
package main

import "fmt"

func CaptureLoop() {
	f := make([]func(), 3)
	fmt.Println("CaptureLoop")
	for i := 0; i < 3; i++ {
		f[i] = func() {
			fmt.Println(i)
		}
	}

	for i := 0; i < 3; i++ {
		f[i]()
	}
}

func CaptureLoop2() {
	f := make([]func(), 3)
	fmt.Println("CaptureLoop2")
	for i := 0; i < 3; i++ {
		v := i
		f[i] = func() {
			fmt.Println(v)
		}
	}

	for i := 0; i < 3; i++ {
		f[i]()
	}
}

func main() {
	CaptureLoop()
	CaptureLoop2()
}

 

 

  • 아래 예제에서는 writeHello함수가 있는데 인자로 넘어온값이 func(string)의 함수형인자이다.
  • writeHello 에서는 해당 write 함수를 호출한다.
  •  
//ch21/ex21.7/ex21.7.go
package main

import (
	"fmt"
	"os"
)

type Writer func(string)

func writeHello(writer Writer) { // ❷ writer 함수타입 변수 호출
	writer("Hello World")
}

func main() {
	f, err := os.Create("test.txt")
	if err != nil {
		fmt.Println("Failed to create a file")
		return
	}
	defer f.Close()

	writeHello(func(msg string) {
		fmt.Fprintln(f, msg) // ❶ 함수 리터럴 외부 변수 f 사용
	})
}