golang 에 대해 알아보자.

Intro

  • 겸사겸사 정리
  • Tucker 의 Go 언어 프로그래밍 책을 기본으로 함.

정리

  • 2007년에 개발을 시작해 2009.11.10에 공개된 오픈소스 언어이다.
  • go 는 정적 컴파일 언어이기에 각 플랫폼에 맞는 실행 파일을 따로 만들어 줘야하고, 가비지컬렉터를 제공하고 있다.
    • java 처럼 VM위에서 돌아가지 않는다 go 의 GC에 대해선 다시 정리하기.
  • hello world 시작
    • 폴더 생성
      • go 는 패키지 단위로 작성됨, 같은 폴더에 위치한 .go 파일은 모두 같은 패키지에 포함됨. 패키지명으로 폴더명을 사용함.
        • 폴더가 다르면 패키지도 달라짐.
    • .go 파일 작성
    • Go 모듈 생성
      • 빌드하기 전에 모듈을 생성해야함.
        • 모듈 생성은 go mod init 명령으로 실행. go mod init 뒤에 모듈 이름을 적어주면 됨.
      • 모듈을 생성하면 각종 정보가 들어있는 go.mod 파일이 생성됨.
    • 빌드
      • go bulid 는 기계어로 변환하여 실행파일을 만듬.
      • 사용중인 시스템에서 실행되는것을 만들기 위핸 그냥 go build 만 하면 됨.
        • 다른 운영체제에서 실행되는 파일을 만들기 위해선 옵션 몇가지만 주면 됨.
          • go tool dist list 명령으로 확인 가능.
    • 샘플코드 분석
//패키지 선언.  main은 특별취금 main() 이 있어야만 main 패키지 사용가능.
package main

//특정 패키지를 불러오기.
import "fmt"

// go 는 main() 함수로부터 시작되고 main() 함수가 종료되면 프로그램이 종료됨.
func main() {
  fmt.Println("hello world)

}

변수

  • 아래의 예시를 보자
var a int  = 1
//var 는 변수 선언 키워드, a 는 변수명, 그 이후에 변수 타입과 =뒤의 초깃값
  • 숫자 타입
uint8 : 1바이트 부호없는 정수 : 0~255
uint16 : 2바이트 부호없는 정수 : 0~65535
...
byte : uint8의 별칭 : 0~255
rune : int32의 별칭 UTF-8 문자 하나를 타나낼때 사용.
  • 그외 타입
    • 블리언, 문자열, 배열, 슬라이스(가변 길이 배열), 구조체, 포인터, 함수타입(함수 포인터), 인터페이스(메서드 정의의 집합), 맵, 채널 이 있다.
  • 변수 선언
var a int = 1 //기본 형태
var b int   // 초깃값 생략. 기본값으로 세팅된다.
vac c = 2 //타입 생략. 우변 값으로 타입이 된다. 
d := 3 // 선언 대입문 := 를 사용해 var 키워드 생각한다.

표준 입출력

  • fmt 패키지
    • Print() : 함수 입력값들을 출력함
    • Println(): 함수 입력값들을 출력하고 개행함.
    • Printf() : 서식(format)에 맞도록 출력함.
      • 서식지정자
        • %v : 데이터 타입에 맞춰서 기본 형태로 출력함.
        • %d : 10진수 정숫값으로 출력
        • %s : 문자열 출력
        • %p : 메모리 주솟값을 출력함.
  • 표준입력
    • 키보드 입력과 Scan() 함수의 동작 원리
      • 사용자가 표준입력장치(키보드 등)로 입력하면 입력 데이터는 컴퓨터 내부의 표준입력 스트림(standard input stream)이라는 메모리 공간에 임시 저장됨.
      • Scan() 함수는 그 표준 입력스트림에서 값을 읽어서 입력값을 처리함.

함수

  • 아래와 같은 형태를 가짐.
    • 함수명중 첫 글자가 대문자인 경우는 패키지 외부로 공개되는 함수임.
    • 함수 코드 블록의 시작을 알리는 중괄호 { 는 함수를 정의하는 라인과 항상 같은 줄에 위치해야함.
    • 함수를 호출하는 인수는 매내변수로 복사된다.
      • 값 전달, 레퍼런스 전달 중 go 는 값 전달만 지원함. 변수 전달이 복사로만 이루어짐.
func Add(a int, b int) int {
  return a + b
}
  • 아래와 같이 별티 반환 함수도 가능함
c, success := Add(3, 5)
...
func Add(a int, b int) (int, bool) {
  return a + b, true
}
  • 아래와 같이 변수명을 지정해 반환도 가능함.
    • return 만 호출하면 됨.
func Add(a, b int) (result int, success bool) {
  result = a + b
  success = false
  return
}

상수

  • go 에서 상수로 사용할 수 있늩 타입은 불리언, 룬, 정수, 실수, 복소수, 문자열이있음. 아래처럼 사용하면 됨.
    • 상수명도 함수 외부에 선언되어있고, 첫 글자가 대문자인 상수는 패키지 외부로 공개되는 상수입.
const ConstValue int = 10
  • 상수와 리터럴
    • 리터럴이란 고정된 값, 값 자체로 쓰인 문구
      • 아래에서 hello, 0 과 같이 고정된 값 자체로 쓰인 문구가 리터럴임
      • go 에서는 상수는 리터널과 같이 취급됨.
        • 상수의 메모리 주소값에 접근할 수 없는 이유는 컴파일 타임에 리터럴로 변환 되기 때문임.
var str string = "hello"
var i int = 0

if 문

  • if 초기문; 조건문 도 가능하다 아래 예시를 보자
// filename, success := UploadFile() 이 초기문, success 이 조건문.
if filename, success := UploadFile(); success {
  ...
}

switch 문

  • const 열거값에 따라 수행되는 로직을 변경할때 switch 문을 주로 사용한다. 아래 예시를 보자.
type ColorType int //별칭 ColorType 을 선언하고 const 열거값 정의
const (
  Red ColorType = iota
  Blue
)
func colorToString(color ColorType) string {
  switch color {
    case Red:
      return "Red"
    case Blue:
      return "Blue"
    default :
      return "etc"
  }
} 
  • 다른 언어는 break 문이 있어야 하지만, go 는 case 하나 실행 후 자동으로 switch 를 빠져나온다.
  • 만약 다음 case 까지 실행시키고 싶으면, case 마지막에 fallthrough 키워드를 사용하면 된다.

for 문

  • go 는 반복문으로 for 하나만 지원한다. 아래와 같은 여러 형태가 있다.
for 초기문; 조건문; 후처리 {
  코드블록
}

// 초기문 생략
for ; 조건문; 후처리 {
  코드블록
}

// 후처리 생략
for 초기문; 조건문;  {
  코드블록
}

// 조건문만 있는 경우
for ; 조건문;  {
  코드블록
}

// 무한 루프 : 조건문이 true 이다. 
for true {
  ...
}

// true 도 생략 가능하다. 
for {
  ...
}
  • 레이블
    • for 문을 사용할때 레이블을 정의하고 break 할때 정의한 레이블을 적어주면 그 레이블에서 가장 먼저 속한 for 문까지 모두 종료하게 된다.
OuterFor:
  for {
    for {
      if a == b {
        break OuterFor // 레이블에 가장 먼저 포함된 for 문까지 종료
      }
    }
  }

배열

  • 최대 요소 개수를 생성할 때 고정되어 중간에 늘리거나 줄일 수 없다.
  • 배열은 값을 여러 개 저장하는 연속된 메모리 공간.
var 변수명 [요소 개수]타입

// int 타입 요소를 5개 갖는 배열 numbs 를 할당, 초깃값 지정안해서 0으로 세팅됨.
var nums [5] int

// string 타입 배열 3개 생성, 마지막은 "" 으로 초기화될듯.
days := [3]string{"monday", "tuesday"}

// int 타입 5개를 갖는 배열 s를 할당. 인덱스가 1인 요솟값을 10으로, 3인 요소값을 30으로 초기화, 나머지는 0으로 초기화됨.
var s = [5]int{1:10, 3:30}
//s[0] = 0, s[1] = 10, s[2] = 0, s[3]=30

// ...를 사용하면 배열 요소 개수를 생략할 수 있음.
x := [...]int{10, 20, 30}
  • for 문에서 range 키워드를 사용하면 배열 요소 순회한다.
var t [5]float64 = [5]float64{1.0, 2.0, 3.0, 4.3, 5.3}
for i, v:= range t {
  fmt.Println(i, v)
}

구조체

  • 구조체는 다음과 같은 형식을 가짐
  • 구조체의 타입명의 첫번째 글자가 대문자이면 패키지 외부로 공개되는 타입임.
  • 구조체 안의 필드명이 대문자로 시작하는 경우는 패키지 외부로 공개되는 필드임.
    type 타입명 struct {
    필드명 타입
    ...
    필드명 타입
    }
    
  • 구조체 초깃값을 생략하면 모든 필드가 기본값으로 초기화됨.
  • 구조체의 크기
type User struct {
  Age int// 8바이트
  Score float64 // 8바이트
}

// 아래 구조체의 메모리 크기는 16바이트가 됨.
var user User
  • 구조체 복사
    • go 내부에서는 필드 각각이 아닌 구조체 전체를 한번에 복사함. 대입 연산자가 우변 값을 좌변 메모리 공간에 복사할 때 복사되는 크기는 타입 크기와 같음.
type User struct {
  Age int// 8바이트
  Score float64 // 8바이트
}

...
var user User{3, 2.3}
user3 := user //구조체 복사.
  • 필드 배치 순서에 따라 구조체 크기 변화
    • go 에서도 메모리 정렬 이슈로 int 형과 float64 형을 가진 구조체의 크기는 16 바이트로 되어버림.
      • 메모리 정렬을 위해 필드 사이의 공간을 띄우는 것을 메모리 패딩(memory padding)이라고 함.

포인터

  • 포인터 변수 선언은 데이터 타입 앞에 * 를 붙여서 선언.
  • 기본값은 nil 이다.
var p *int
var a int
p = &a // a의 메모리 주소를 포인터 변수 p에 대입
*p = 10 // p 가 가리키는 메모리 공간의 값을 
  • == 연산을 사용해 포인터가 같은 메모리 공간을 가리키는지 확인 가능하다.
var a int = 10
var b int = 30
var p1 *int = &a
var p2 *int = &a
var p4 *int = &b

if(p1 == p2){
  ...
}
  • 변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 사용하는 문제와 큰 메모리 공간을 복사할 때 발생하는 성능 이슈가 있음. 아래 예시를 보자.
type Data struct {
  value int
  data [200] int
}

func Change(arg Data) {
  arg.value = 900
  arg.data[100] = 999
}

func main() {
  var data Data
  Change(data)
  //data 를 출력하면 0 만 출력되는것을 볼 수 있다. (value, data[100] 둘다. )
}

// 값이 복사되어 전달되기 때문에 각기 다른 메모리 공간에 존재한다. 
// data 변숫값이 모두 복사되기에 구조체 크기만큼 복사된다. Data 구조체는 int 타입과 value 크기가 200인 int 배열로 구성되어있어 총 1608바이트이다. 
// Change 이 호출될 때마다 1608 바이트가 복사된다. 

// 아래처럼 바꾸는게 좋다. 
func Change(arg *Data) {
  arg.value = 900
  arg.data[100] = 999
}
Change(&data)
// data를 출력하면 999로 모두 바뀌었다. 이제 data 변수값이 아니라 data 의 메모리 주소를 인수로 전달한다. 메모리 주소는 64bit 컴퓨터에서 8바이트 숫자값이기에 8바이트만 복사가 된다. 

  • Data 구조체를 생성해 포인터 변수 초기화 하기
    • 구조체 변수를 별도로 생성하지 않고, 포인터 변수에 구조체를 생성해 주소를 초기값으로 대입하는 방법을 알아 보자.
// 기존 방식
var data Data
var p *Data = &data 

//구조체를 생성해 초기화하는 방식
var p *Data = &Data{}// *Data 타입 구조체 변수 p 를 선언한다.
  • 할당된 메모리 공간의 실체를 인스턴스라고 부른다.
    • 쓸모없는 인스턴스를 메모리에서 해제하기 위해서 go 언어에서는 가비지 컬렉터라는 메모리 정리 기능을 제공한다.
  • new() 내장 함수를 이용해 초기화 할 수도 있다.
    • new() 내장 함수는 type 를 인수로 받는다.
      p1 := &Data{} // & 를 사용하는 초기화
      var p2 = new(Data) // new() 를 사용하는 초기화. 내부 필드값ㅇ르 원하는 값으로 초기화 할 수 없다. 
      
  • 대부분 프로그래밍 언어에서 메모리를 할당할 땐 스택 메모리 영역이나 힙 메모리 영역을 사용한다.
    • 이론상 스택 메모리 영역이 힙 메모리 영역보다 효율적이지만, 스택 메모리는 함수 내부에서만 사용 가능한 영역이다.
      • C/C++ 에서는 new, malloc 함수를 호출해 힙 메모리 공간을 할당, 자바에서는 클래스 타입을 힙에 기본 타입을 스택에 할당.
      • go 언어는 탈출검사(escape analysis)를 해서 어느 메모리에 할당할지를 결정한다.
        • go 의 스택 메모리는 계속 증가되는 동적 메모리 풀이다.
  • 함수 외부로 공개되는 인스턴스의 경우 함수가 종료되어도 사라지지 않는다.
type User struct {
  Age int
}
func NewUser(age int) *User {
  var u = User{age}
  return &u // 탈출 분석으로 u 메모리가 사라지지 않음.
}

문자열

  • go 는 UTF-8 문자코드를 사용한다. (유니코드의 일종으로 가변길이 문자 인코딩 방식이다. 한문자가 1~3바이트이다.)
    • utf-16이 한문자에 2byte 를 고정사용하는 대신, 영문자, 숫자등은 1byte 그외 문자는 2~3byte 로 표현한다. ANSI와 1:1 대응이 된다.
  • 문자 하나를 표현하는데 rune type 이 사용된다.
    • rune 타입은 4바이트 정수 타입인 int32 타입의 별칭 type 이다.
  • len() 내장함수는 문자열 크기를 알 수 있는데, 문자 수가 아니라 문자열이 차지하는 메모리 크기이다.
  • []rune 타입 변환으로 글자수를 알 수 있다.
    • string type, rune slice type인 []rune type 은 상호 타입 변환이 가능하다.
    • string type 과 []byte 타입도 상호 타입 변환이 가능하다.
str := "hello 월드"
runes := []rune(str) // []rune 타입으로 타입 변환
len(runes) // 8
len(str) // 12
  • string 타입은 연속된 바이트 메모리라면 []rune 타입은 글자들의 배열로 이루어저 있다.
  • string 구조 알아보자.
    • sting 내장타입으로 내부 구현은 감춰져 있지만 reflect 패키지안의 StringHeader 구조체를 통해 내부 구현을 엿볼수 있다. 내부 구조는 아래와 같다.
    • 첫번째 필드인 Data 는 문자열 데티어가 있는 메모리 주소를 나타내는 일종의 포인터
    • 두번째 필드인 Len은 문자열의 길이를 나타냄.
      type StringHeader struct {
      Data uintptr
      Len int
      }
      
  • 위와 같은 구조이기 때문에 string 끼리 대입을 하면 문자열을 하나하나 복사하는 것이 아니라
    • 구조체 변수가 복사도리때 구조체 크기만큼 메모리가 복사된다.
    • Data 포인터 값과 Len 값이 복사된다.
  • 문자열은 불변이다.
    • 즉 string 타입이 가리키는 문자열의 일부만 변경할 수 없다.
      • []byte 슬라이스 타입으로 변환하면 일부만 변경 가능하다.
        • go 에서는 슬라이스 타입변환을 할 때 문자열을 복사해서 새로운 메모리 공간을 만들어 슬라이스가 가리키도록 한다. (문자열 불변 원칙이 지켜진다.)
    • 문자열을 합칠때에도 기존 문자열 메모리 공간을 건들이지 않고, 새로운 메모리 공간을 만들어 두 문자열을 합친다.
      • 문자열 합치는 연산이후의 string 주소값은 바뀌게 된다. (문자열 불변 원칙이 지켜진다.)
      • string 합 연산이 빈번하게 일어나면 메모리 낭비가 된다.
        • 이럴경우는 string 패키지안의 Builder 를 이용해 메모리 낭비를 줄일 수 있다.
        • strings.Builder 는 내부에 슬라이스를 가지고 있기에 문자를 더할 때 매번 메모리를 새로 생성하지 않고, 기존 메모리 공간에 빈 자리가 있으면 그냥 더하게 된다.

패키지

  • go 언어에서 코드를 묶는 가장 큰 단위
  • main 패키지 하나와 여러 외부 패키지의 조합
    • 프로그램 시작점을 포함한 패키지.
  • 패키지 찾기 좋은 곳 (awesome go) 에서 필요한 것들을 찾아보자.
    • https://github.com/avelino/awesome-go
  • 여러가지 패키지 사용법 예시
//소괄호로 패키지를 묶어 임포드 시킬수 있다.
import (
  "fmt"
  "os"
  "math/rand" //패키지 명은 math 이다. 

  "text/template" // 같은 이름이라면.
  htmltmeplate "html/template" // 별칭을 붙여서 해결할수 있다.

  _ "github.com/something" // 직접 사용하지 않는 패키지는 _ 을 붙이면 된다.
)
...

rand.Int() // 경로가 있는 패키지에 접근할땐 마지막 폴더명인 rand 만 사용한다.
htmltmeplate.New()
  • 패키지 설치하기.
    • import 로 패키지를 포함하면 go build 를 통해 빌드할때 해당 패키지를 찾아 포함한 다음 실행파일을 생성한다.
    • go 는 import 된 패키지를 어떻게 찾을까?
        1. 기본 제공하는 패키지는 go 설치 경로에서 찾는다.(go 설치시 같이 설치된다.)
        1. 깃허브와 같은 외부 저장소의 저장된 패키지는 외부 저장소에서 다운 받아 GOPATH\pkg 폴더에 설치한다. (go 모듈에 정의된 패키지 버전에 맞게 다운로드 한다.)
        1. 현재 모듈 아래 위치한 패키지인지 검사한다. 현재 모듈 아래 위치한 패키지는 현재 폴더 아래 있는 패키지를 찾는다.
  • go 모듈
    • go 모듈은 go 패키지를 모아놓은 go 프로젝트 단위. 모든 go 코드는 go 모듈 아래 있어야 함.
    • go build 를 하려면 반드시 go 모듈 루트 폴더에 go.mod 파일이 있어야 함.
      • go.mod 파일은 모듈 이름과 go 버전, 필요한 외부 패키지 등이 명시
      • go 는 go build 를 통해 실행파일을 만들때 go.mod 와 외부 저장소 패키지 버전정보를 담고있는 go.sum 파일을 통해 외부 패키지와 모듈 내 패키지를 합처 실행 파일을 만들게 됨.
    • go 모듈은 go mod init 명령어를 통해 만들 수 있음.
      • 이 명령어를 실행하면 go.mod 파일이 생성됨.
go mod init [패키지명]
  • go mod tidy
    • go 모듈에 필요한 캐피지를 찾아서 다운로드 해주고 필요한 패키지 정보를 go.mod 파일과 go.sum 파일에 적어주게 된다.
  • 실행해보기
    • goproject/usepkg 폴더를 만들고 그 폴더 안에서 go mod init goproject\usepkg 를 실행하자.
  • 패키지명과 패키지 외부 공개
    • 대문자로 선언된 첫 글자로 시작하는 모든 변수, 상수, 타입, 함수, 메서드는 패키지 외부로 공개된다.
  • 패키지 초기화
    • 패키지를 임포트 하면.
        1. 컴파일러는 패키지 내 전역 변수를 초기화 함.
        1. 그런 다음 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 함.
          • init() 함수는 반드시 입력 매개변수가 없고, 반환값도 없는 함수여야 한다.
          • 만약 어떤 패키지의 초기화 함수인 init() 함수 기능만 사용하기를 원할 경우 밑줄 _ 을 이용해서 패캐지를 임포트 하자.

슬라이스

  • 일반적인 배열은 처음 배열을 만들때 정한 길이에서 더이상 늘어나지 않는 문제가 있다. 슬라이스는 배열과 비슷 하지만, [ ] 안에 배열의 개수를 적지 않고 선언한다.
    • 슬라이스를 초기호 하지 않으면 길이가 0인 슬라이스가 만들어 진다.
var slice []int
  • 슬라이스 초기화 방법
      1. 배열처럼 { } 를 사용해 요솟값을 지정하자.
      1. 내장함수인 make() 를 이용한 초기화, 첫번째 인수로 타입을, 두번째 인수로 길이를 적어준다.
var slice1 = []int{1,2,3} // 1 2 3
var slice2 = []int{1, 5:2, 10:3} // 1 0 0 0 0 2 0 0 0 0 3

//아래 2개는 서로 다른 타입을 만든다.
var array = [...]int{1,2,3}//배열 선언
var slice = []int{1,2,3}//슬라이스 선언

var slice = make([]int, 3)
  • 슬라이스 요소 추가 - append()
    • 첫 번째 인수로 추가하고자 하는 슬라이스를
    • 두 번째 인수로 요소를 적어주면 슬라이스 맨 뒤에 요소를 추가해 만든 새로운 슬라이스를 결과로 반환한다.
    • 여러 값도 추가 가능하다.
      • slice = append(slice, 1,2,3,4) 처럼 여러개의 값을 적으면 된다.
  • 슬라이스 동작 원리
    • 슬라이스는 내장 타입으로 내부 구현은 감춰져있지만 reflect 패키지의 SliceHeader 구조체를 사용해 내부 구현을 살펴볼 수 있다.
type SliceHeader struct {
  Data uintptr  // 실제 배열을 가리키는 포인터
  Len int // 요소 개수
  Cap int // 실제 배열의 길이
}
  • make() 함수를 사용해 슬라이스를 만들때 인수를 2개 혹은 3개를 넣는데 어떻게 다를까?
    • make( [ ] int, 3) 처럼 하면 slice의 len도 3이고 cap도 3이다.
    • make( [ ] int, 3, 5) 처럼 하면 slice의 len은 3이고 cap는 5이다.
  • 슬라이스와 배열의 동작 차이
    • go 에서 모든 값의 대입은 복사로 이루어진다. 함수에 인수로 전달될 때나 다른 변수에 대입을 할때나 값의 이동은 복사이다.
    • 복사는 타입의 값이 복사가 된다.
      • 포인터는 포인터의 값인 메모리 주소가 복사
      • 구조체가 복사될때는 구조체의 모든 필드가 복사된다. 배열은 배열의 모든 값이 복사된다.
    • 배열과 슬라이스를 함수의 인자로 넘겨 특정 값을 변경을 하면 이런한 이유로 차이가 발생한다.
      • 슬라이스의 경우 포인터와 len, cap값이 복사되는 것이다. 슬라이스에 실제 데이터가 저장되어있는 배열은 복사되지 않는 영역이다.
  • 슬라이싱 (배열의 일부를 집어내는 기능)
    • 슬라이싱 기능을 사용하면 그 결과로 슬라이스를 반환한다.(슬라이싱은 동사, 슬라이스는 그 결과인 명사로 보자.)
    • 배열의 일부를 집어낼때는 대상이 되는 배열을 쓰고 대괄호 사이에 집어내고자 하는 시작 인덱스 : 끝 인덱스 를 쓴다.
      • 이러면 시작인덱스 부터 끝인덱스 -1 까지의 배열 일부를 나타내는 슬라이스가 반환된다.
    • 배열뿐 아니라 슬라이스 일부를 집어낼 때에도 사용 가능하다.
array[startIndex:endIndex]

//
slice1 := []int{1,2,3,4,5}
slice2 := slice1[0:3] // slice2 는 [1,2,3]

// 만약 첫번째 부터 슬라이싱을 하면 시작인덱스를 생략할 수 있다. 아래 2개는 같다.
slice2 := slice1[0:3]
slice2 := slice1[:3]
slice3 := slice[:]  // 전체 슬라이스이다 끝 인덱스도 생략하였다.
  • 슬라이싱을 하면 그 결과로 배열 일부를 가리키는 슬라이스를 반환한다.
    • 새로운 배열이 만들어 지느 ㄴ것이 아니라, 배열의 일부를 포인터로 가리키는 슬라이스를 만들어 낼 뿐이다.
  • 슬라이싱 기능 활용
2개의 슬라이스가 서로 같은 배열을 가리켜 문제가 발생할수 있다. 항상 다른 배열을 가리키게 하려면 어떻게 하면 될까?

//1. 하나의 슬라이스를 순회해서 하나씩 복사하는 방법
//2. slice2 := append([]int{}, slice1...) 처럼 append() 함수를 사용해 slice1 의 모든 값을 복제한 새로운 슬라이스를 만들어 slice2 에 대입하기. 
// 배열이나 슬라이스 뒤에 ... 을 하면 모든 요솟값을 넣어준 것과 같게 된다. 
//3. 내장함수 copy() 를 사용하는 방법
func copy(dst, src[]Type) int

특정 요소 삭제하기
// 특정 요소를 삭제 한 다음에 
// append 를 이용해 슬라이스를 시프트 시키자. 

특정 요소 추가하기
//슬라이스 중간에 요소 하나를 추가하려면. 
//슬라이스 맨뒤에 요소 하나를 추가후. 원하는 위치부터 하나씩 시프트 시키는 방식을 사용한다.
//slice = append(slice[:idx], append([]int{100}, slice[idx:]...)...)
// 불필요한 메모리 사용이 없도록 코드를 개선하면 아래처럼 된다.
slice = append(slice, 0)// 맨뒤에 요소 추가
copy(slice[idx+1:], slice[idx:]) // 값 복사
slice[idx] = 100 // 값 변경.

슬라이스 정렬

  • int 형 슬라이스 정렬
    • sort 패키지의 Ints() 함수를 사용하면 된다.
    • Float64s() 를 사용하면 float64 슬라이스를 정렬할 수 있다.
  • 구조체 슬라이스 정렬
    • Sort() 함수를 사용하기 위해선 Len(), Less(), Swap() 세 메서드가 필요하다. 이것들만 구현하면 우리가 정의한 구조체도 정렬할 수 있다.
type Student struct{
  Name string
  Age int
}
...
type Sutdents []Student
func (s Students) Len() int {return len(s)}
func (s Students) Less(i,j int) bool {return s[i].Age < s[j].Age}
func (s Students) Swap(i,j int) {s[i], s[j] = s[j], s[i]}
...
s := []Student{ {"1", 1}, {"2", 2}, {"3", 3}}
sort.Sort(Students(s)) // Age 로 정렬.

메서드

  • 메서드는 함수의 일종. go 에는 클래스가 없다. 그래서 구조체 밖에 메서드를 지정한다.
  • 구조체 밖에 메서드가 있으므로 어떤 구조체에 속하는지 표시할 방법이 필요한데, 이때 리시버가 사용된다.
  • 메서드 선언
    • 메서드를 선언하려면 리시버를 func 키워드와 함수 이름 사이에 소괄호로 명시해야 한다.
// (r Rabbit) 가 리시버이고
// info() 가 메서드명 이다. 
// 이제 info() 는 Rabbit 타입에 속한다고 알게 되었다.
func (r Rabbit) info() int {
  return r.w * r.h
}

// 일반 함수 표현
func info(r Rabbit) int {
  return r.w * r.h
}
// 메서드 표현
func (r Rabbit) info() int {
  return r.w * r.h
}
  • go 는 클래스와 상속을 지원하지 않고 메서드와 인터페이스만 지원한다.
  • 포인터 메서드 vs 값 타입 메서드
    • 리시버를 값 타입과 포인터로 정의할수 있다. 이 둘의 차이는
type account struct {
  balance int
}
// 포인터 메서드
func(a1 *account) withdrawPointer(amount int){
  a1.balance -= amount
}
// 값 타입 메서드
func(a2 account) withdrawValue(amount int){
  a2.balance -= amount
}

// 값 타입 메서드
func(a3 account) withdrawReturnValue(amount int) account{
  a2.balance -= amount
  return a3
}

var test *account = &account{100}
test.withdrawPointer(30)  // 70 이다
test.withdrawValue(20)  // 여전히 70 이다.
test.withdrawReturnValue(20) // 50 이다. 
  • 포인터 메서드를 호출하면 포인터가 가리키고 있는 메모리의 주솟값이 복사됨.
  • 값 타입 메서드를 호출하면 리시버 타입의 모든 값이 복사됨. 리시버 타입이 구조체이면 구조체의 모든 데이터가 복사됨.
  • 값 타입 메서드와 포인터 메서드는 어떤 경우에 사용하면 좋을까?
    • 포인터 메서드는 메서드 내부에서 리시버의 값을 변경시킬 수 있다.
    • 값 타입 메서드는 호출하는 쪽과 메서드 내부의 값은 별도 인스턴스로 독립이 되기에 메서드 내부에서 리시버의 값을 변경시킬 수 없다.

인터페이스

  • 구현을 포함하지 않는 메서드 집합
  • 인터페이스 선언
    • type 을 쓴뒤 인터페이스 명을 쓰고 interface 키워드를 쓴다. 그 뒤 중괄호 { } 안에 인터페이스에 포함된 메서드 집합을 써 준다.
    • 인터페이스도 구조체처럼 타입중 하나이기에 type 을 써줘야 한다
type DuckInterface interface {
  // 메서드는 반드시 메서드 명이 있어야 한다.
  // 매개변수와 반환이 다르더라도 이름이 같은 메서드는 있을 수 없다.
  // 인터페이스는 메서드 구현을 포함하지 않는다.
  Fly()
  Walk(distance int) int
}


// 실제 예제
//Stringer 인터페이스 선언
type Stringer interface {
  String() string
}
type Student struct {
  Name string
}
//  Student 의 String 메서드
func (s Student) String() string {
  return "a"
}

func main() {
  student := Student{"name"}//Student 타입
  var stringer Stringer //Stringer 타입
  stringer = student  //stringer 값으로 student 대입
  stringer.String() //stringer 의 String() 메서드 호출
}
  • go 에서는 ~er 를 붙여 인터페이스명을 만드는 것을 권장함.
  • go 언어에서는 어떤 타입이 인터페이스를 포함하고 있는지 여부를 결정할 때 덕 타이핑 방식을 사용
    • 타입 선언시 인터페이스 구현 여부를 명시적으로 나타낼 필요없이 인터페이스에 정의한 메서드 포함 여부만으로 결정하는 방식.
    • 덕타이핑이란
      • 특정 타입의 인터페이스 구현 여부를 타입 선언시 미리 명시하지않아도 그 타입이 인터페이스에서 정의한 메서드들을 모두 포함하고 있다면, 그 인터페이스로 간주하는 방식
  • 인터페이스 기능 더 알기
    • 포함된 인터페이스
    • 빈 인터페이스
    • 인터페이스 기본값 : nil 이다.
// 인터페이스도 다른 인터페이스를 포함 할 수 있다. 
// Reader와 Write 인터페이스를 선언하고 이둘을 포함한 ReadWrite 인터페이스를 선언하는 예시를 보자.
type Reader interface {
  Read() (n int, err error)
  Close() error
}
type Writer interface {
  Write()(n int, err error)
  Close() error
}
type ReadWriter interface {
  Reader  //Reader 의 메서드를 포함
  Writer //Writer 의 메서드를 포함
}


// interface{} 는 메서드를 가지고 있지 않는 빈 인터페이스.
// 모든 타입이 빈 인터페이스로 쓰일 수 있음.
// 빈 인터페이스는 어떤 값이든 받을 수 있는 함수, 메서드, 변숫값을 만들때 사용함. 아래 예시를 보자. 
func PrintVal(v interface{}) {
  //v 의 타입에 따라 다른 로직을 수행한다.
  switch t := v.(type) {
    case int:
    case float64:
    case string:
    default :
  }

}


// 인터페이스를 사용할 때 항상 인터페이스 값이 nil이 아닌지 확인해야 하낟. 
type A interface {
  b()
}
func main() {
  var a A
  a.b() //a가 nil이기에 런 타임 에러 발생.
}
  • 인터페이스 변환하기. 인터페이스 변수를 타입 변환을 통해 구체화된 다른 타입이나 다른 인터페이스로 변환할수있다.
    • 구체화된 다른 타입으로 타입 변환하기
      • 인터페이스를 본래의 구체화된 타입으로 복원할때 주로 사용함. 사용법은 인터페이스 변수 뒤에 점을 찍고 소괄호 안에 변경하려는 타입을 써주면 됨.
      • 인터페이스 변수를 구체화된 타입으로 변환하려면 해당 타입이 인터페이스 메서드 집합을 포함하고 있어야 함. 아니면 컴파일 타임 에러 발생.
    • 다른 인터페이스로 타입 변환하기.
      • 인터페이스 변환을 통해 구체화된 타입뿐 아니라 다른 인터페이스로 타입 변환 할 수 있음.
      • 이 때는 구체화된 타입으로 변환할 때와 달리 변경되는 인터페이스가 변경 전 인터페이스를 포함하지 않아도 됨. 인터페이스가 가리키고 있는 실제 인스턴스가 변환하고자 하는 다른 인터페이스를 포함해야 함.
// a 인터페이스 변수를 ConcreteType 타입으로 변환해 변수 ConcreteType t를 생성하고 대입
var a Interface
t := a.(ConcreteType)


// 구체화된 다른 타입으로 타입 변환하기 변환 샘플
// 인터페이스
type Stringer interface {
  String() sting
}
// 구조체
type Student struct {
  Age int
}

//Student 타입의 String() 메서드
func (s *Student) String() string{
  return "a"
}

//
func Print(stringer Stringer) {
  s := stringer.(*Student)  // *Student 타입으로 타입 변환
  s.Age 
}

func main() {
  s := &Student{2}  //*Student 타입 변수 s 선언 및 초기화
  Print(s)  // s 를 인터페이스 인수로 Print() 함수 호출
}


// 다른 인터페이스로 타입 변환하기. 샘플
// ConcreteType 이 AInterface 와 BInterface 모두 포함하고 있는 경우 변환 가능함.
var a AInterface = ConcreteType{}
b := a.(BInterface)
  • 타입 변환 성공 여부 반환
    • 타입 변환 반환값을 2개의 변수로 받으면 타입 변환 가능 여부를 두 번째 반환값 으로 알려줌. 이때 타입 변환이 불가능하더라도 두 번째 반환값이 false로 반환될뿐 런타임 에러는 없다.
var a Interface
t, ok := a.(ConcreteType) //t 는 타입 변환한 결과, ok 는 변환 성공 여부임.

함수 고급

  • 가변 인수 함수
    • … 키워드를 사용하면 가변 인수 처리가 된다.
//가변 인수를 받는 함수.
func sum(nums ...int) int {
  // 내부에서 nums 는 int 슬라이스 타입 [] int 로 처리됨.
  for _, v := range nums {
    ...
  }
}
  • 여러 타입을 인수로 한번에 섞어쓰는 함수같은 Print 도 있는데 이는 빈 인터페이스 에 있다
func Print(args ... interface{}) string{
  for _, arg := range args {
    switch f : = arg.(type)
    case bool:
    case int:
    ...
  }
}
  • defer 지연 실행
    • 바로 실행이 아닌 해당 함수가 종료되기 직전에 실행되도록 하기 위해서 사용되는 명령어이다.
    • defer 는 역순으로 호출 된다.
func main() {
  defer fmt.Println("반드시 호출됩1")
  defer fmt.Println("반드시 호출됩2")
  defer fmt.Println("반드시 호출됩3")
}
// 실행 결과는 아래와 같다.
// 반드시 호출됩3
// 반드시 호출됩2
// 반드시 호출됩1
  • 함수 타입 변수
    • 함수 포인터 라고 생각하면 되는 변수.
    • 함수 타입은 함수명과 함수 코드 블록을 제외한 함수 정의로 표시된다.
func add(a, b int) int {
  return a+b
}

//함수 포인터는 함수명인 add 와 코드 블록인 {...} 사이를 제외하고 다음과 같이 표현한다.
func (int, int) int

// 예제를 보자.
func add(a, b int) int {
  return a+b
}

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

//op에 따른 함수 타입 반환
func getOperator(op string) func(int, int) int {
  if op == "+"{
    return add
  }
  return mul
}

func main() {
  var operaotr func (int, int) int
  operator = getOperator("*")

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

//별칭으로 아래처럼 줄여쓸수도 있다.
type opFunc func (int, int) int
func getOper(op string) opFunc

  • 함수 리터럴
    • 이름없는 함수로 함수 타입 변숫값으로 대입되는 함숫값을 의미
    • 익명 함수, 또는 람다 라고 불림.

에러 핸들링

  • fmt 패키지의 Errorf() 함수를 이용하면 원하는 에러 메시지를 만들 수 있음. 또는 errors 패키지의 New() 함수를 이용해서 error 를 생성할수 있음
func New(text string) error
//그래서 아래처럼 사용가능
errors.New("에러 메시지")
  • error 는 인터페이스로 문자열을 반환하는 Error() 메서드로 구성됨.
    • 즉, 어떤 타입이든 문자열을 반환하는 Error() 메서드를 포함하고 있으면 에러로 사용할 수 있음.
type error interface {
  Error() string
}


//아래와 같은 예제를 만들수 있음.
type AError struct{ // 에러 구조체 선언
  Len int
  RequireLen int
}

func (err AError) Error() string {//
  return "a가 이상해요."
}

func Bmetohd(name string) error {
  if(len(name) < 3 ){
    return AErrorP{len(name), 3} // error 반환.
  }
  return nil
}

func main(){
  err := Bmetohd("input_name")
  if err != nil {
    if errInfo, ok := err.(AError); ok { //인터페이스 변환.
      ...
    }
  }
}
  • 에러 랩핑
  • 에러를 감싸서 새로운 에러를 만들어야 할 수도 있다.
    • 예를들어 파일에서 텍스트를 읽어서 특정 타입의 데이터로 변환하는 경우, 파일 읽기에서 발생하는 에러도 필요하지만, 텍스트의 몇번째 줄의 몇번째 칸에서 에러가 발생했는지 알면 더 유용할 것이다.
  • 패닉
    • 프로그램을 정상 진행시키기 어려운 상황을 만들었을때 프로그램 흐름을 중지시키는 기능.
    • 잘못된 메모리에 접근하거나 그러면 프로그램 실행이 불가능 할 수 있다. 이럴땐 프로그램을 강제 종료하는편이 좋다.
    • panic() 함수를 사용해 패닉을 발생시키거나 go 내부에서 패닉이 발생하기도 한다.
      • 콜스택을 알 수 있다.
    • 패닉 발생후 바로 종료해도 되지만, 복구할수있는 것이라면 복구를 시도하는게 좋다.
      • panic 은 호출 순서를 거슬러 올라가 전파된다.
        • 만약 main() -> f() -> g() 였고, g 에서 패닉이 발생하면 호출 순서를 거꾸로 올라가며 함수로 전달된다.
        • main함수까지 복구되지 않으면 프로그램은 그때 강제 종료가 된다.
        • 어느 단계든 패닉은 복구된 시점부터 프로그램이 계손된다.
        • recover() 함수를 호출해 패닉 복구를 할 수 있다.
          • recover 함수가 호출되는 시점에 패닉이 전파중이면 panic 객체를 반환하고, 그렇지 않으면 nil 반환한다.
        • recover 는 제한적으로 사용하는 것이 좋다.
// 패닉의 함수 선언.
func panic(interface{})

//아래와 같은 경우 다 가능하다.
panic(34)
panic("error")
panic(SomeType{someData})

//recover 함수 선언
func recover() interface{}
//recover 도 interface{} 타입을 반환한다. recover 로 반환한 타입을 사용하려면 다음과 같이 타입 검사를 하자.
if r, ok := recover().(net.Error); ok {
  ...
}

고륀과 동시성 프로그래밍

  • 프로그램 시작점임 main() 함수도 고루틴에 의해서 실행된다.
  • thread 가 전환될 때마다 thread context 를 저장하고 복원하기에 전환 비용이 든다.
  • go 에서는 cpu 코어마다 os 스레드를 하나만 할당해서 사용하기에 컨텍스트 스위치 비용이 발생하지 않는다.
  • 고루틴을 생성하는 구문은 아래처럼 이다.
  • sync 패키지의 WaitGroup 객체를 사용하면 고루틴이 종료될때 까지 대기할 수 있다.
go 함수_호출


func A() {
  ...
}

func main() {
  go A()  //새로운 고루틴 생성
  go A()  //새로운 고루틴 생성
}

//
var wg sync.WaitGroup
wg.Add(3) //작업 개수 설정
wg.Done() //작업이 완료될 때마다 호출
wg.Wait() //모든 작업이 완료될 때까지 대기.
  • 고루틴의 동작 방법
    • 고루틴과 스레드 간의 관계를 알아보기 위해 2개의 코어를 가진 컴퓨터에서 고루틴이 어떻게 동작하는지 알아보자.
      • 고루틴이 하나일때
        • 코어 1 - os thread 1 - 고루틴 1
      • 고루틴이 2개일때
        • 코어 1 - os thread 1 - 고루틴 1
        • 코어 2 - os thread 2 - 고루틴 2
      • 고루틴이 3개일때
        • 이러면 3번째 고루틴은 남는 코어가 생길때까지 실행되지 않고 멈춰있다.
        • 코어 1 - os thread 1 - 고루틴 1
        • 코어 2 - os thread 2 - 고루틴 2
        • 고루틴 3
      • 시스템 콜 호출시
        • 시스템 콜을 호출하면 os 에서 해당 서비스가 완료될 때가지 대기한다.
        • 기존에 대기하고 있던 고루틴이 동작하게 된다.
    • 결국 컨텍스트 스위치 비용이 발생하지 않는다.
      • 코어가 스레드를 변경하지 않고 오직 고루틴만 옮겨 다니기 때문.
  • 뮤텍스
    • 한 고루틴이 값을 변경할 때 다른 고루틴이 건들지 못하게 하는방법중 하나가 뮤텍스를 이용 하는 것이다.
    • 뮤텍스의 Lock() 메서드를 호출해 뮤텍스를 획득할 수 있다. Unlock() 메서드로 반납한다.
    • 이미 Lock() 메서드를 호출해서 다른 고루틴이 뮤텍스를 획득했다면 나중에 호출한 고루틴은 앞의 뮤텍스가 반납할때까지 대기하게 된다.

채널과 컨텍스트

  • 채널이란 고루틴끼리 메시지를 전달할 수 있는 메시지 큐이다. 메시지 큐에 메시지들은 차례대로 쌓이게 되고 메시지를 읽을 때는 맨 처음 온 메시지부터 차례대로 읽게 된다.
  • 채널을 사용하기 위해선 먼저 채널 인스턴스를 만들어야 한다.
    • 채널은 슬라이스, 맵 등과 같이 make() 함수로 만들수 있다. 채널 타입은 채널을 의미하는 chan과 메시지 타입을 합쳐서 표현한다.
  • 채널에 데이터 넣기
    • 채널에 데이터를 넣을때 <- 연산자를 이용한다.
  • 채널에서 데이터 빼기
    • 채널에서 데이터를 뺄때도 <- 연산자를 이용한다.
  • 일반적으로 채널을 생성하면 크기가 0인 채널이 만들어짐.
  • 채널이 더이상 필요없으면 close를 호출해서 닫아준다.
var messages chan string = make(chan string)
// 채널 인스턴스 변수 : messages
// 채널 타입 : chan string
// 채널 키워드 : chan
// 메시지 타입 : string


messages <- "this is a message"
// 채널 인스턴스 : messages
// 연산자 : <-
// 넣을 데이터 : "this is a message"


var msg string = <- messages
// 빼낸 데이터를 담을 변수 :  var msg string
// 연산자 : <-
// 채널 인스턴스 : messages



//아래는 채널 사용하는 얘시이다.
func main() {
  var wg sync.WaitGroup
  ch := make(chan int)  // 채널 생성

  wg.Add(1)
  go square(&wg, ch)  // 고루틴 생성
  ch <- 9 // 채널에 데이너 넣음
  wg.Wait() // 작업이 완료되길 기다림.
}
func square(wg *sync.WaitGroup, ch chan int) {
  n := <- ch  // 채널에서 데이터 빼옴, 현재 채널에는 아무런 데이터가 없기 때문에 데이터가 들어올때까지 대기하게 됨.
  time.Sleep(time.Second) //1초 대기 
  wg.Done()
}



// 크기가 0인 채널 생성 예시.
func main() {
  ch : make(chan int) // 크기가 0인 채널 생성
  ch <- 9 // main 함수가 여기서 멈춤, 채널에 데이터를 넣었지만, 보관할 곳이 없어 다른 고루틴에서 데이터를 빼가기를 기다림.
  fmt.Print("never print")
}


// 버퍼를 가진 채널 생성 
var chan string messages = make(chan string, 2) // 버퍼 크기 2로 만들기.
//버퍼가 다 차면 버퍼가 없을때 와 마찬가지로 빈자리가 생길때까지 대기하게 됨. 고루틴 멈춤.


// 아래와 같은 사용법도 가능하다.
select {
  case n1 := <- ch1:
    ... //ch1 채널에서 데이터를 빼낼 수 있을 때 실행
  case n2 := <- ch2:
    ... //ch2 채널에서 데이터를 빼낼 수 있을 때 실행
}