220520

HyeonKi Jo·2022년 5월 20일
0
post-thumbnail

7. 타입, 메서드, 인터페이스

7.2 메서드

7.2.1 포인터 리시버와 값 리시버

  • 위에서 포인터리시버에서 포인터를 지우고 그냥 값 리시버로 사용하면, 값에 변경이 일어나지 않는다.
    • 주소가 아니라, 그냥 값이 호출되기 때문에 변경해도 main에는 영향이 없기 때문이다
  • 현재 시간과 업데이트 횟수를 출력하는 구조체와 리시버는 똑같지만, 그 동작을 함수에서 실행했을때의 차이점이다.
    • 똑같은 코드지만 doUpdateWrong은 값을 파라미터로 전달받고, (원본에 영향을 주지 않음)
    • doUpdateRigth은 주소를 파라미터로 전달받는다.
      (원본에 영향을 줌, 원하는 동작)
    • 주소를 전달 받았을 때에 원하는대로 동작하는 것을 볼 수 있다.

7.2.2 nil 인스턴스를 위한 메서드 작성

  • 포인터 메서드를 사용해야 한다.
  • 초기화가 되지 않은, 제로값인 자료형에 nil이 들어간다.
  • nil인스턴스
    • Array, Map, 구조체, Pointer 등의 타입들이 초기화되지 않았을 떄, nil인스턴스를 가진다.
  • 이진트리를 생성하고, 입력된 값을 이진탐색하여 포함 되는지 안되는지 출력하는 코드이다.
    • 포인터 리시버를 사용하여, nil인스턴스 메소드를 사용할 수 있다.

7.2.3 메서드도 함수이다.

  • 메서드를 변수에 할당하거나 함수의 입력으로 반환값으로 사용 가능
    -> 메서드 값(Method value)
  • 메서드 (표현)식
    -> 타입 자체로 함수 생성.
package main

import (
	"fmt"
)

type Adder struct {
	start int
}

func (a Adder) AddTo(val int) int {
	return a.start + val
}	

func main() {
	myAdder := Adder{
		start: 10,
	}
	fmt.Println(myAdder)
	fmt.Println(myAdder.AddTo(5))
	fmt.Println(myAdder)

	f1 := myAdder.AddTo	// 메서드지만 함수이기도 하다. (method value)
	fmt.Printf("%T\n", f1)	// 그래서 타입이 func(int)로 나온다.
	fmt.Println(f1(10))	// 또한, 함수이기 때문에 이렇게 사용할 수 있다.

	f2 := Adder.AddTo	// 위 f1은 myAdder라는 구조체 변수를 하나 생성한 후 그 변수의 AddTo함수를 사용한 것이지만, 이번엔 함수를 직접 가져온다.
	fmt.Printf("%T\n", f2)	// 그래서 타입이 func(int)로 나온다.
	fmt.Println(f2(10))	// 또한, 함수이기 때문에 이렇게 사용할 수 있다.
}
  • 위 코드에서는 f2에서 Adder의 AddTo함수를 가져왔지만, AddTo에 Adder구조체 값을 전달하지 못해 에러가 나온다.
package main

import (
	"fmt"
)

type Adder struct {
	start int
}

func (a Adder) AddTo(val int) int {
	return a.start + val
}	

func main() {
	myAdder := Adder{
		start: 10,
	}
	fmt.Println(myAdder)
	fmt.Println(myAdder.AddTo(5))
	fmt.Println(myAdder)

	f1 := myAdder.AddTo	// 메서드지만 함수이기도 하다. (method value)
	fmt.Printf("%T\n", f1)	// 그래서 타입이 func(int)로 나온다.
	fmt.Println(f1(10))	// 또한, 함수이기 때문에 이렇게 사용할 수 있다.

	f2 := Adder.AddTo		// 위 f1은 myAdder라는 구조체 변수를 하나 생성한 후 그 변수의 AddTo함수를 사용한 것이지만, 이번엔 함수를 직접 가져온다.
	fmt.Println(f2(myAdder, 10))	// Adder의 AddTo함수를 사용했지만, Adder구조체의 값이 필요하기 때문에 기존에 있던 Adder구조체 변수를 사용
	fmt.Println(f2(Adder{start:20}, 10)) // 또는 이렇게 간단하게 구조체를 생성하여 사용할 수 있다.
}

7.2.4 함수와 메서드 비교

  • 구분을 하는 요소는 함수가 다른 데이터에 의존적인지 여부이다.
  • 로직이 단순하게 입력 파라미터에만 영향을 받는다면 함수로 구현하면 된다.
  • 그러나 시작할 떄 설정되거나, 프로그램 수행중에 계속 변경되는 값에 의존한다면 (포인터에 의존?) 메소드를 사용하는 것이 좋다.

7.2.5 타입 선언은 상속되지 않는다.

  • 다른 타입 기반으로 (사용자 정의) 타입 선언
    • 상속이 아니다.
    • 계층이라는 개념이 없다.
    • 타입들은 서로 동등한 레벨이다.
  • 기본 타입이 내장 타입인 사용자 정의 타입의 경우 해당 기본타입의 연산자와 함께 사용할 수 있다.
  • 또한 기본 타입과 호환되는 리터럴 및 상수를 대입할 수 있다.

7.2.6 타입은 실행가능한 문서

  • 사용자 타입을 선언하는 시점
      1. 구조체의 경우 우리가 필요한 시점
      1. 기존 타입을 기반으로 사용자 정의 타입을 선언하는 것은
      • "타입은 문서이다"를 기준으로 삼자
      • 개념을 위한 이름을 타입 이름으로 지정하여 코드를 더 명확하게 하고, 기대되는 데이터의 종류를 기술할 목적으로 사용한다.

7.2.7 열거형을 위한 iota

  • 열거형 (enumeration, enum)
    • 기본값은 정수이지만, 그리고 정수를 사용할 수는 있지만 권장하지 않는다.
    • 카테고리성 데이터, 명목형 데이터를 나타내기 위함
      • 도시 - 서울, 부산, 인천 등... (열거)
      • 이메일 종류 - 미분류 메일, 스팸메일, 소셜메일, 비즈니스 메일....
city := []{'서울', '부산', '인천', '대전'}

mycity := '부산'

switch myCity{
	//case city[0]:		// 이런 방식으로 사용하면 인덱스별로 어떤 원소가 있는지 알고 있어야 한다.
    case "서울"		// 그러나 이렇게 사용할 수 있다.
    	"서울 출신이시네요"
    if 서울 < 부산 // 이 비교연산도 가능하다. 인덱스 값으로 비교된다.
}	
  • Go에서 열거형 만들기
    1. 모든 유효한 값을 나타내는 정수 기반의 타입 정의
    1. 값의 집합을 만들기 위해 const 블록 사용
    1. const 블록에서 첫 번째 상수에 iota 지정
  • 이때, const에서 값이 추가거나 수정되면 아래 코드들도 찾아서 모두 수정해야 한다.
package main

import (
	"fmt"
)

type MailCategory int

const (					// const블록은 상수 변수를 여러개 정의할 때 사용한다.
	Uncategorised	MailCategory = 10
	Personal	MailCategory = iota
	Spam
	Social
	Advertisements
)

func main() {
	//myMailTitle := "이것 한 번 드셔봐. 애들은 가라!"

	// 로직 : 메일 타이츨을 보고 메일의 종류를 지정
	myMailCat := Advertisements
	fmt.Println(myMailCat)

	switch myMailCat {
		case Uncategorised :
			fmt.Println("미분류 메일", Uncategorised )
		case Spam:
			fmt.Println("짭짤한 메일", Spam)
		case Personal:
			fmt.Println("개인적 메일", Personal)
		case Advertisements:
			fmt.Println("광고 메일", Advertisements)
	}
}
  • iota를 사용하면 자동으로 상수 타입으로 값을 만들어준다.
    • 따로 정수를 입력해주지 않고, 값의 이름으로 구분만 해주면된다.
    • iota를 사용하기 전에 정수를 따로 배정할 수 도 있다.
    • 값이 명시적으로 (다른곳에) 정의 되어있는 상수를 정의하는데 iota를 사용하지 말자.
    • iota는 0부터 번호가 시작된다. 따라서, 위 처럼bit연산으로 사용할 수 있다. 하드웨어에서 상태 체크할 때 사용할 수 있다.

7.3 구성(composition)을 위한 임베딩 사용

  • Composition over inheritance
    • 클래스 상속 보다는 객체 구성 선호
    • 결국 같은 코드를 여러번 입력하지 않고, 한번만쓰자, 코드의 재사용성을 위한 것이다
package main

import (
	"fmt"
)

type Employee struct {
	Name	string
	ID	string
}

func (e Employee) Description() string{
	return fmt.Sprintf("%s (%s)", e.Name, e.ID)
}

type Manager struct {			// 매니저도 직원중에 하나이기 때문에 Employee의 하위라고 할 수 있다. (개념상)
	Employee					// 보통 구조체니까, Emp Employee 이런식으로 사용하지만, '임베딩'으로 사용하기 위해 타입만 쓴다.		
	Reports []Employee
}

func main() {
	m := Manager{				// Manager변수 m을 생성한다.
		Employee: Employee{		// Manager에 임베딩된 Employee구조체의 초기화를 진행한다.
			Name: 	"Bob Bobson",
			ID: 	"12345",
		},
		Reports: []Employee{},	// Manager의 요소인 report를 정의한다.
	}

	fmt.Println(m.Employee.ID)	// m 내부의 Employee 구조체의 ID를 출력, Employee의 ID
	fmt.Println(m.ID)			// 그러나 Employee가 임베딩되어 있기 때문에 그냥 m.ID로 사용해도 된다. 만약 따로 사용한다면, m.Employee.ID, m.ID 처럼 따로 사용할 수도 있다.
	fmt.Println(m.Description())// Employee의 메소드또한 사용할 수 있다.
}

7.4 임베딩은 상속이 아니다.

  • GO는 동적 디스패치(Dynamic Dispatch)가 없다.
package main

import "fmt"

type Inner struct {
	A int
}

func (i Inner) IntPrinter(val int) string {	// Inner타입의 int형을 받아서 출력하고 문자열 반환하는 함수
	return fmt.Sprintf("Inner: %d", val)
}

func (i Inner) Double() string {		// Inner타입의 문자열을 받아서 두배한 후, IntPrinter에 보냄
	result := i.A * 2
	return i.IntPrinter(result)
}

type Outer struct {				// Inner를 임베딩하고 문자열을 추가
	Inner
	S string
}

func (o Outer) IntPrinter(val int) string {	// InterPrint의 Outer버전
	return fmt.Sprintf("Outer: %d", val)
}

func main() {					// Outer 정의
	o := Outer{
		Inner: Inner{			// 임베딩된 Inner정의
			A: 10,			// Outer의 문자열 추가
		},
		S: "Hello",
	}
	fmt.Println(o.Double())			// Double은 Outer의 메소드가 아니지만, Inner가 임베딩 되어있기 때문에 사용할 수 있다.
    								// 여기서 Outer의 IntPrinter가 아니라 Inner의 Printer가 나온다.
}
  • 임베딩된 항목의 메소드는 포함하는 구조체의 메소드 집합에 포함된다. 이것은 포함하는 궂체에서 인터페이스를 구현할 수 있다는 것을 의미한다.

7.5 인터페이스

  • 암묵적 인터페이스
    • GO의 유일한 추상 타입
  • 인터페이스의 목적 / 용도
    • 스펙 정의
    • 인터페이스를 사용하려면 인터페이스의 요소를 완성 / 구현(implement)해야 한다.
    • 유럽에 가면 유럽의 콘센트의 규격이 있을 것이다. 이 정의된 인터페이스를 맞춰야 사용할 수 있다.
    • 이를 통해 타입의 안정성을 높이고 디커플링을 가능 (솔천커지)
  • C++: 추상클래스를 인터페이스로 사용하였다.
  • interface 키워드
interface {
......
}
  • 위 ... 에는 인터페이스를 만족시키기 위한 구체적 타입에서 반드시구현(implement) 해야 할 메소드 시그니처 나열
  • GO에서 관례적으로 인터페이스 이름은 끝에 'er'을 붙인다.
  • 인터페이스는 모든 블록에서 선언 가능하다.

7.6 인터페이스는 타입에 안정적인 덕 타입핑이다.

  • 덕 타이핑
    • 동적 타이핑의 한 종류로, 객체가 어떤 타입에 걸맞은 변수와 메소드를 지니면, 객체를 해당 타입에 속하는 것으로 간주한다는 것이다.
    • 객체지향 프로그래밍언어에서는
  • 데코레이터 패턴
    • 함수나 메소드를 실행할 때, 실행되기 전 혹은 후에 먼저 자동으로 실행해야하는 함수가 있는 패턴.
    • 인터페이스와 defer를 이용해 데코레이터 패턴을 구현할 수 있다.

7.7 임베딩과 인터페이스

  • 구조체에 타입을 임베딩 할 수 있는 것처럼, 인터페이스에 인터페이스를 임베딩 할 수 있다.
  • Reader 인터페이스는 Read함수를 구현하면 사용할 수 있고,
  • Closer역시 Close를 구현하면 사용할 수 있다.
  • ReadCloser는 Reader와 Closer를 타입만써서 임베딩하여 사용할 수 있다.

7.8 인터페이스를 받고 구조체 반환하기

  • 디자인 패턴: program to interface not to function(함수)
  • GO: 인터페이스를 받고 구조체를 반환해라
    => 함수로 실행되는 비즈니스 로직은 인터페이스를 통해 실행
    => 함수의 출력은 구체 타입이어야 함

7.9 인터페이스와 nil

  • s는 빈 포인터이며, 제로값을 nil을 가지고 있다.
  • i는 빈 인터페이스이며, 제로값을 nil을 가진다.
  • 빈 인터페이스는 0개 이상의 타입을 가지고 있을 수 있다.

7.11 타입 단언과 타입 스위치

  • 인터페이스 변수가 할당된 구체 타입 확인
  • 구체 타입이 다른 인터페이스를 구현했는지 확인
  • 타입 단언 (Type Assertion)
    • 인터페이스 변수. (타입)
  • 타입 스위치 = 인터페이스.(type) + switch 문
  • 위와 같이 콤마 OK관용구를 사용하여 이런 것을 회피할 수 있다.
    • 이때 ,(콤마) OK 관용구를 사용할 수 있다.
    • 또,

10. Go의 동시성 (Concurrency)

  • 동시성은 컴포넌트가 안전하게 데이터를 공유하는 방법을 지정하는 콤퓨터 과학 용어이다.
  • 대부분의 언어는 잠금(Lock)을 획득하여 공유데이터에 접근한다.
  • 그러나 GO는 Go의 가장 유명한 기능인 순차적 프로세스들의 통신 (CSP, Communicating Sequential Process) 에 기반한다.

10.1 동시성 사용 시점

  • 동시성은 병렬성이 아니다.
    • 동시성 코드가 병렬적(동시에)으로 실행되는지는 하드웨어와 알고리즘에 의해 결정된다.
  • 동시성은 동시에 실행되는 것이 시간이 얼마 걸리지 않을 때 사용하는 것은 좋지 않다.
  • 동시성은 공짜가 아니다.

10.2 고루틴

  • 프로세스
  • 스레드
  • OS의 스케쥴러 및 Go 런타임의 스케줄러
    • Go 런타임 스케줄러의 장점 (p.279~280)
  • 고루틴 : Go 키워드
    • Go 런타임에서 관리하는 가벼운 프로세스
    • 비즈니스 로직을 구현한 클로저를 고루틴으로 실행하는 것이 관례
    • 모든 함수는 Go루틴으로 실행될 수 있다.

동기(Synchronous) VS 비동기(asynchronous)

  • 동기
    • 싱크를 맞춘다.
    • 무전기, 상대방이 준비 됬을 때 통신한다.
    • 동기 I/O
      • 버퍼만큼 읽고, 버퍼를 비워야 다시 읽을 수 있다.
  • 비동기
    • 그냥 전화처럼 서로 상대방을 기다릴 필요없이 같이 말해도 된다.
    • 비동기 I/O
      • 막 와서 읽는다.

10.3 채널

  • 고루틴(스레드)은 채널을 통해 통신한다.
  • 채널에서 받을 데이터와 채널에서 나갈 데이터를 선언한다.
  • make 함수로 생성
  • 내장 타입
  • 채널의 제로 값은 슬라이스, 맵, 포인터 처럼 nil
  • <- 연산자를 사용하여 채널과 상호작용한다.
    • val <- chan : 채널로부터 데이터 읽기
    • chan <- val : val의 값을 chan에 쓴다.
  • 기본적으로 채널은 버퍼가 없다.
    • 버퍼가 없기 때문에, 데이터 하나가 전송되서 상대방이 읽어야 다른 데이터를 또 보낼 수 있다.
    • 그래서 데이터 전송이 막혀서 다른 데이터를 전송하지 못한다. 이를 블락(Block)상태 라고 한다.
  • 버퍼 있는 채널 : make에서 수용량을 조정할 수 있다. 그러나 역시 수용량이 꽉차도록 통신이 안되면 Block상태에 빠진다.
  • for - range를 주로 사용한다.
  • 채널 쓰기를 완료했을 때, Close내장함수를 이용해 채널을 닫을 수 있다.
    • 이때, 닫힌 채널에 쓰기를 시도하거나, 다시 닫으려 한다면 패닉을 발생시킨다.
    • 그러나 닫힌 태널에 읽기를 시도한다면 성공한다.
      • 채널이 버퍼링되고, 아직 읽거자기 않는 값이 있다면 값들이 순서대로 반환된다.
      • 채널에 버퍼가 없거나, 채널에 더이상 값이 없을 때, 채널타입의 제로값을 반환한다.
      • 이때, 만약 채널이 닫혀서 0이 반환되는 건지, 값이 0인건지 알 수 없다.
      • 이때 OK관용구를 사용하여 닫힘 유무를 확인할 수 있다-
  • 채널을 닫아야하는 책임은 채널에 쓰기를 하는 고루틴에 있다.

10.4 Select 문

  • 만약 두개의 동시성 연산을 수행해야 하는데, 한쪽으로만 연산이 쏠린다면, 한쪽은 사용하지 않게 될 것이다. 이를 기아(starvation)상태라고 한다.
  • Select알고리즘은 단순하다.
    • 진행이 가능한 여러 case중 하나를 임의로 선택한다.
    • 임의로 선택하기 때문에 기사문제를 깔끔하게 해결한다.
    • 또한, 교착상태 (deadlock)를 해당 프로그램을 제거하는 방법으로 해결한다.
package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan int)	//채널 1생성
	ch2 := make(chan int)	//채널 2 생성
	go func() {
		v := 1		// v에 1대입
		ch1 <- v	// 채널1에 v를 저장한다.
		v2 := <-ch2	// v2에 채널2를 대입한다.	// v2는 값이 아직 없다면 입력할떄까지 대기한다.
		fmt.Println(v, v2)	// v와 v2를 출력한다.
	}()
	v := 2			//v에 2 대입

	ch2 <- v		//채널2에 v넣기
	v2 := <-ch1		// v2에 채널1 넣기  	// 채널1에 값이 아직 없다면 입력할 떄까지 대기한다.

	fmt.Println(v, v2)	// v와 v2 출력
}
  • 교착상태에 빠져서 exit status 2출력과 함께 에러가 출력되었다.

package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan int)	//채널 1생성
	ch2 := make(chan int)	//채널 2 생성
	go func() {
		v := 1		// v에 1대입
		ch1 <- v	// 채널1에 v를 저장한다.
		v2 := <-ch2	// v2에 채널2를 대입한다.	// v2는 값이 아직 없다면 입력할떄까지 대기한다.
		fmt.Println(v, v2)	// v와 v2를 출력한다.
	}()
	v := 2			//v에 2 대입
	var v2 int
	select{
		case 	ch2 <- v:		//채널2에 v넣기
		case	v2 = <-ch1:		// v2에 채널1 넣기  	// 채널1에 값이 아직 없다면 입력할 떄까지 대기한다.
	}
	fmt.Println(v, v2)	// v와 v2 출력
}
  • Select case문으로 교착상태를 해결하였다.
profile
Talking Potato

0개의 댓글