Sad Puppy 3 'Golang' 카테고리의 글 목록 :: 개발자 아지트

 

Go 언어를 통해 웹 서버 만들기

 

Go 언어에서는 net/http 패키지를 통해 웹 서버를 쉽게 만들 수 있다.

 

웹 서버를 만들기 전에 각 경로에 대한 핸들러를 등록한 후 시작해야 한다. 

해당 과정을 통해 핸들러를 등록 한 후, 웹 브라우저에서 경로를 입력하면 HTTP 요청을 서버로 전송할 수 있다. 

 

웹 서버 테스트 코드는 httptest패키지를 사용해 작성할 수 있다. 

 

HTTP를 사용하면 HTML 및 다양한 데이터를 전송할 수 있다. 

데이터 전송시 JSON 포맷을 많이 사용한다. 

 

 

RESTful API 서버 만들기

 

REST란 여러가지의 웹 아키텍처를 합친 개념을 말한다. 

 

RESTful API를 만들기 위해서 URL로 자기표현식 데이터를 나타낼 수 있다. 

 

데이터 동작을 정의하기 위해 HTTP 메서드를 이용해야 한다. 

 

어떤 서비스라도 일관된 표현식으로 표현하기 위해서는 REST를 사용하면 된다. 

 

RESTful API를 이용하면, 데이터 규약이 통일되고, 이를 통해 많은 서비스 간 연결이 가능하다. 

 

 

 

통상적으로 프론트엔드, 백엔드, 데이터베이스 3계층으로 구성된 것을 웹 서비스라고 하며, 3티어 라고 한다. 

 

  • 프론트엔드
    • 사용자가 화면을 볼 때, 보이는 부분이고 HTML, CSS, JavaScript로 만들어진다.
  • 벡엔드
    • 사용자가 화면을 보기 위해 사용자가 보이지 않는 곳에서 화면 구성에 필요한 데이터와 동작 로직을 제공하는 계층이다. Go를 사용하면 쉽게 만들 수 있다. 
  • 데이터베이스
    • 데이터베이스 종류는 다양하며, 데이터를 저장하는 계층이다. 

 

총체적으로 Go는 백엔드에 특화된 프로그래밍 언어이다. 

 

웹 서버를 테스트용으로 배포할 때 헤로쿠를 사용할 수 있다. 헤로쿠는 무료이며, PaaS 서비스를 제공한다. 

 

 

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 29장~31장을 읽고 공부한 글입니다

 

SOLID: 객체지향 설계 원칙

 

객체지향 설계 5원칙을 SOLID라고 한다.

 

  1. 객체는 하나의 목표에 대한 책임을 저야한다는 것이 단일 책임 원칙이다.
  2. 확장에는 열려있고, 변경에는 닫혀 있어야 한다는 것이 개방-폐쇄 원칙이다. 
  3. 상위 타입을 인수로 받는 함수는 하위 타입 인수에도 작동해야 한다는 것은 리스코프 치환 원칙이다. 
  4. 불필요한 메서드에 의존적이지 않아야 한다는 것은 인터페이스 분리 원칙이다. 
  5. 구체화된 객체는 추상화된 객체와 의존 관계를 가져야 한다는 것은 의존 관계 역전 원칙이다. 

 

테스트와 벤치마크

 

Go언어에서는 자체 테스트 코드 작성 및 실행을 지원한다. 

 

테스트 코드 작성에는 3가지 규칙이 존재한다. 

 

  1. 테스트 코드는 _test.go 로 끝나는 파일 안에 존재해야 한다.
  2. 테스트 코드는 testing 패키지를 가져와야 한다. 
  3. 테스트 코드는 func TestXxxx(t *testing.T)형식으로 작성해야 한다. 

테스트를 실행 할 때는 go test 명령을 통해 실행한다. 

 

테스트 작업은 세세하고 많을 수록 좋다. 

 

테스트 주도 개발을 통해 자연스럽게 테스트 코드를 작성하는 것이 좋다. 

 

 

 

코드의 성능 측정 방법을 벤치마크 코드라고 한다. 

 

벤치마크 코드는 func BenchmarkXxx(b *testing.B) 형식으로 작성해야 한다. 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 27장~28장을 읽고 공부한 글입니다. 

고루틴, 동시성 프로그래밍

 

 

고루틴은 경량화 된 스레드로서 컨텍스트 스위칭 비용이 발생하지 않는다. 

 

코어가 여러개인 멀티 코어 환경에서는 고루틴을 여러개 사용해 성능을 높일 수 있다. 

 

고루틴 여러개가 같은 메모리 영역을 조정할 경우, 예상치 못한 문제가 발생 가능하다. 

 

뮤텍스는 고루틴 하나만 동시에 자원에 접근할 수 있도록 조정한다. 

 

뮤텍스를 잘못 사용할 경우, 데드락에 문제가 발생할 수 있다. 

 

Go에서 뮤텍스 없이 동시성 프로그래밍을 하기 위해 작업 분할 방식 혹은 역할 분할 방식을 통해 작업할 수 있다. 

 

 

 

 

채널, 컨텍스트

 

채널은 고루틴 간의 메시지를 전달하는 메시지 큐이다. 

이를 통해 뮤텍스 없이도 동시성 프로그래밍이 가능하다.

 

동시성 프로그래밍에서 생산자와 소비자 패턴이 많이 사용되며, 이는 Go에서 채널을 이용해 구현 가능하다. 

 

컨텍스트란?

 

작업자에게 일을 지시할 때 사용하는 일종의 작업 명세서이다. 

이를 통해 특정 시간 동안만 작업을 지시하거나, 외부에서 작업 취소가 가능하다. 

 

만약 채널을 제때 닫지 않을 경우, 무한 대기를 지속하는 좀비 루틴이 발생되어 프로그램 성능을 떨어트리고 메모리 사용을 지속적으로 증가시키는 문제가 생길 수 있다. 

 

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 24장~25장을 읽고 공부한 글입니다. 

자료구조

 

  • 배열
    • 원소마다 메모리가 연속적으로 할당되어 있다.
  • 리스트
    • 메모리 할당이 연속적이지 않다. 
    • 원소 추가 및 삭제의 시간복잡도는 O(1)로, 배열에 비해 처리가 빠르다. 
    • 시작과 끝이 연결되어 원형 구조를 이룬다. 
    • 키, 값의 쌍으로 이루어진 데이터 구조이다. 
    • 데이터를 빠르게 처리할 수 있다. 

 

 

에러 핸들링

 

Go에서는 Error 인터페이스를 통해 에러를 반환한다. 

 

에러는 errors.New() 혹은 fmt.Errorf()함수를 통해 만들 수 있다. 

사용자 정의 타입으로 Error()메서드를 정의해 error 객체를 만들 수 있다. 

 

에러를 감싸서 새로운 에러를 만들수도 있고(에러 랩핑),  감싸진 에러는 다시 풀어서 에러 처리를 할 수 있다. 

감싸진 에러를 다시 풀 때는 errors 패키지의 As()함수를 통해 사용한다. 

 

 

프로그램을 사용하다가 계속 사용하기 어려운 상황에 직면할 경우, 프로그램의 흐름을 중지시키는 기능을 패닉이라고 한다. 

Go에서는 프로그램을 즉시 종료할 때 내장함수인 panic()함수를 통해서 하고 복구는 recover() 함수를 통해 할 수 있다. 

 

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 22장~23장을 읽고 공부한 글입니다. 

인터페이스

 

인터페이스란?

인터페이스는 자바의 클래스 비슷하지만 다른 개념임. 

메서드 구현도 가능하고 추상화된 객체로 사용가능함(인터페이스는 메서드의 집합체이다. )

객체지향 프로그래밍에서 가장 중요한 역할을 함.

인터페이스를 이용할 경우, 구체적인 객체가 아닌 인터페이스 만으로도 메서드를 호출할 수 있음.
따라서 프로그램 변경사항 발생시 유연하게 변경가능함.

 

 

인터페이스 사용법

// 타입 선언 키워드, 인터페이스 명, 인터페이스 선언 키워드, {} 내부에는 메서드 집합을 기입해줌

type MeInterface interface{
	Eat()
    Walk(distance int) int 
}

 

인터페이스도 구조체와 같이 타입중 하나임으로 type 키워드를 써준다. 

즉, 인터페이스 변수 선언 및 변수처럼 값으로 사용 가능하다. 

 

 

메서드 집합 사용시 유의사항 

type Tmp interface{
	String() string
    String(int) string // 메서드명을 중복되게 사용할 수 없음
    _(y int) // 메서드는 반드시 이름 필요함
}

 

 

인터페이스 사용 예시

package main
import "fmt"

type Stringer interface {
	String() string
}

type Student struct {
	Name string
    Age  int
}

func (s Student) String() string { 
// student 타입은 Stringer의 String() 메서드를 포함함 따라서, 
// Student타입은 Stringer 인터페이스로 사용될 수 있음

	return fmt.Sprintf("Hi My name is %s, my age is %d.", s.Name, s.Age)
    // Sprintf 함수는 문자열을 만들어 String타입의 값으로 반환하는 함수
}

func main(){
	student := Student{ "Minsoo", 101 }
    var stringer Stringer // stringer은 Stringer인터페이스임
    
    stringer = student 
    //stringer을 통해 Student타입의 student를 갖고 있음으로써 student의 메서드 String이 호출됨
    
    fmt.Printf("%s\n", stringer.String())  
}

 

출력값: Hi My name is Minsoo, my age is 101.

 

 

 

인터페이스에서 정의한 메서드 집합을 가진 모든 타입은 인터페이스로 쓰일 수 있다. 

 

덕 타이핑이란, 인터페이스에서 정의한 메서드 포함 여부로 판단하는 것이다. 

 

인터페이스를 통해 추상화 계층을 만들 수 있으며, 관계로 상호작용을 정의한다. 

 

모든 타입이 비어있는 인터페이스의 변수 값으로 사용될 수 있다. 

 

인터페이스 변환을 통해, 인터페이스 변수에 대해 구체화된 타입이나 또 다른 인터페이스로 변경 가능하다. 

 

 

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 20장~21장을 읽고 공부한 글입니다. 

 

함수 고급

 

인수 타입 앞에 마침표 3개를 찍는 것을 통해 가변 인수를 표현한다. 

 

함수 종료 전 처리해야 하는 로직 수행을 위해 defer을 사용할 수 있다. 

 

함수를 변수 값으로 저장하기 위해 함수 타입 변수를 사용할 수 있다. 

 

쉽게 내부 상태를 갖는 함수 정의를 통해 함수 리터럴을 사용할 수 있다. 

슬라이스

 

  • 동적 배열, 동적 배열이란 자동으로 배열 크기를 증가시키는 자료구조이다. ⇒ 관리 용이
  • 슬라이싱 기능을 이용해 배열의 일부를 나타내는 슬라이스를 만들 수 있음

 


사용법

 

아래와 같이 사용하면 고정된 크기의 배열을 사용하게됨.

var array [10]int

 

슬라이스 사용: 배열의 개수를 적지 않고 선언함.

-아래의 코드를 작성하면, 슬라이스를 초기화 하지 않은것이고, 길이가 0인 슬라이스가 만들어짐

이때, 슬라이스 길이를 초과해서 접근하면 런 타임 에러가 발생함

var slice []int

 

슬라이스 초기화 방법 1.

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

 

슬라이스 초기화 방법 2. make() 내장 함수 사용.

var slice = make([]int, 3) // 타입, 배열의 길이, 각 요소 값은 int타입의 기본값인 0으로 채워짐

 

슬라이스 접근- C에서의 접근과 동일.

슬라이스 순회- 반복문을 통해서 순회함 len함수, range함수 사용

슬라이스 요소 추가- python에서 사용하는 append를 사용, 여러값 추가 가능


슬라이스 동작 원리

 

  • 슬라이스는 배열과 비슷하지만, 제대로된 동작 원리 이해가 필요함
  • 내장 타입이라 내부 구현이 감춰져 있음(reflect 패키지의 SliceHeader 구조체를 사용해 내부 구현을 살펴볼 수 있음)
  • 슬라이스 내부 정의는 다음과 같음

-배열을 가리키는 포인터

-배열 요소 개수를 나타내는 len

-전체 배열 길이를 나타내는 cap

필드로 구성된 구조체임

  • 슬라이스 변수 대입 시 배열에 비해서 사용되는 메모리나 속도에 이점이 있다. (?)

-make함수를 통해 슬라이스를 선언할 수 있음


슬라이스와 배열의 동작 차이

 

동작 차이의 원인

-go 언어에서 모든 값의 대입은 복사로 일어남. (함수의 인수로 전달될 때, 다른 변수에 대입할 때 등)

 

복사는 타입의 값이 복사됨.

 

포인터는 포인터의 값인 메모리 주소가 복사됨

구조체는 구조체의 모든 필드가 복사됨

배열은 배열의 모든 값이 복사됨.

 

슬라이스의 경우, 슬라이스 내부의 배열을 가리키는 포인터, len, cap만 복사되므로, 실제 배열의 값을 복사하는건 아님 .


슬라이싱

 

-배열의 일부를 집어내는 기능

-슬라이싱을 이용하면 슬라이스를 반환함(즉, 잘린 부분)

 

array[startIndex:endIndex]// 이때 슬라이스는 array의 startIndex~endIndex-1까지 슬라이싱 값

 

 

슬라이싱은 슬라이스도 슬라이싱 할 수 있다.

 

슬라이싱시, startIndex 자리를 비워두면 0으로 인식한다.

슬라이싱시, endIndex 자리를 비워두면 특정 요소부터 끝 요소 까지 슬라이싱 한다.

startIndex, endIndex를 둘다 비워두면 대상을 전체에 대해 슬라이싱한다.

 

슬라이싱시, 인덱스 3개를 사용하면 cap의 크기를 조절할 수 있다.

마지막 인덱스를 사용하면 대상 배열의 전체 길이를 사용하는게 아닌, 마지막 인덱스까지만 배열을 사용할 수 있음

 

*배열이나 슬라이스 뒤에 …를 붙이면, 모든 요솟값을 넣어준 것과 같게 됨.

 

copy함수를 이용하여 슬라이스를 복사할 수 있다.


슬라이스 요소 삭제

 

슬라이스의 중간 요소를 삭제하기 위해서는 C 배열에서 특정 인덱스의 값을 삭제할 때 처럼, 삭제한 인덱스 이후의 인덱스에 대해 일일이 값을 당겨줘야 한다.

이때, append()함수를 사용하면 코드를 한줄로 이를 해결할 수 있다.

지우고자 하는 값의 인덱스만 제외하고 append함수를 통해 각 값들을 복사한다.


슬라이스 요소 추가

 

만약, 슬라이스 요소를 슬라이스 중간에 추가 하고자 하는 경우

  • append함수를 통한 슬라이스 맨 뒤에 요소 추가(임의의 값)
  • 맨 뒤 값부터 삽입하려는 인덱스까지의 요소들을 한 칸씩 뒤로 밀어줌(하나씩 뒤로 복사)
  • 삽입하려는 값을 삽입하고자 하는 인덱스로 이동시킨다.

이와 같은 과정을 append()함수를 중첩 사용한 코드 한줄로 사용 가능하다.


슬라이스 정렬

 

-Go 언어에서 기본 제공하는 sort 패키지를 통해 슬라이스를 정렬할 수 있다.

-sort 패키지는 python에서 사용하는 sort함수와 비슷하다.

 

import(
			"sort"
)
//s라는 int타입의 정렬되지 않은 임의의 슬라이스가 있다. 
sort.Ints(s) // 정렬 

// 만약 float타입의 슬라이스를 정렬하고자 한다면 
// Float64s()함수를 사용하면 된다.  

 

-Go에서는 구조체 슬라이스도 정렬할 수 있다.


메서드

 

메서드란?

-함수의 일종

-Go언어에는 클래스가 없는 관계로, 구조체 밖에 메서드를 지정함. 구조체 밖에 메소드를 정의 할때 리시버라는 기능을 사용함

 

리시버란?

-구조체 밖에 메서드가 있다. 따라서 해당 메서드는 어느 구조체에 속하는지 표시해야 하고, 이때 리시버를 사용한다.

즉, 리시버는 메서드가 속하는 타입을 알려주는 기법임.

 

효과는?

-메서드를 사용하여 데이터와 기능이 묶이니 응집도가 높아짐

-코드 재사용성 용이

-모듈화로 코드 가독성이 좋아짐

 

메서드 선언

 

func(r Rabbit) info() int { // r Rabbit은 리시버이고 소괄호로 표시함. info()는 메서드의 이름이다. 
	return r.width * r.height
}

 

 

info() 메서드는 Rabbit 타입에 속한다.

구조체 변수 r은 해당 메서드에서 매개변수처럼 사용된다.

리시버는 모든 로컬 타입(해당 패키지 안에서 type 키워드로 선언된 타입)이 가능함

따라서 패키지 내에 선언된 구조체 및 별칭 타입들이 리시버가 될 수 있다.

 

package main

import "fmt"

type accoutn struct {
	balance int
}

func withdrawFunc(a *account, amount int) { // 함수 표현
	a.balance -= amount
}

func (a *account) withdrawMethod(amout int){ // 메서드 표현 
	a.balance -= amount
}

func main(){
	a:= &account{ 100 } // balnace가 100인 account 포인터 변수 생성
}

withdrawFunc(a, 30) // 함수 형태 호출

a.withdrawMethod(30) // 메서드 형태 호출 

// 위의 함수와 메서드는 똑같은 역할을 하지만 형태가 다르므로, 호출 방법도 다르다.
// 메서드는 해당 리시버 타입에 속한다. 따라서 리시버를 점 연산자를 사용해 호출함

// 구조체 필드에 접근할 때 처럼 . 연산자를 사용해 해당 타입에 속한 메서드를 호출할 수 있다.

 

 

 

별칭 리시버 타입

 

별칭 리시버 타입은 별칭일 뿐 리시버가 될 수 있고, 메서드를 가질 수 있다.

Int와 같은 내장 타입들도 별칭 타입을 활용해서 메서드를 가질 수 있다.

기본 내장 타입들은 별칭 타입으로 변환하여 메서드를 선언할 수 있다.

package main

import "fmt"

// int 타입의 별칭인 사용자 정의 별칭 타입 userInt
type userInt int

// userInt 별칭 타입을 리시버로 갖는 메서드
func (a userInt) add(b int) int {
	return int(a) + b
}

func main() {
	var a userInt = 10
	fmt.Println(a.add(30))
	var b int = 20
	fmt.Println(userInt(b).add(50)) 
    // int 타입과 userInt 타입은 엄연히 다른 타입이므로 연산이 불가함 따라서
    // int 타입을 userInt 타입으로 형변환 하여 사용
}

 

 

메서드가 필요한 이유

 

함수도 있는데 왜 굳이 복잡하게 메서드라는걸 만든걸까?

 

함수와 메서드는 중요한 차이가 있다.

⇒ 소속 유무의 차이, 일반 함수는 어디에도 속하지 않지만, 메서드는 리시버에 속한다.

 

데이터와 관련된 기능을 메서드를 통해 구현하고 이를 데이터와 묶기 좋다.

따라서 코드 응집도를 높이는 중요한 역할을 한다. 좋은 코드는 결합도는 낮고 응집도는 높다. 

메소드는 해당 부분에 중요한 역할을 한다. 

 

코드 응집도가 떨어지면, 새 기능을 추가할 때 흩어진 모든 관련 부분을 검토하고 고쳐야 하는 문제가 발생한다.

코드 수정 범위가 늘어나면 관리는 복잡해지고, 실수가 발생할 확률이 높아지고, 따라서 버그를 만들 확률도 높아진다.

따라서 코드 응집도를 높일 필요가 있다.

 

객체지향 중심으로 변화

메서드가 등장함으로써, 메서드로 데이터와 기능을 묶을 수 있게 되고, 이것은 객체로 동작할 수 있게 됐다.

이는 절차 중심에서 객체 간 관계 중심으로 프로그래밍 패러다임이 변화를 일으켰다. 

과거에는 기능 호출 순서도를 나타내는 플로우차트를 중시했지만, 이제는 객체간 관계를 나타내는 클래스 다이어그램이 더 중요하게 됐다. 

 

객체란 데이터와 기능을 갖는 타입이고, 해당 객체의 타입의 인스턴스를 객체 인스턴스라고 한다. 

이런 객체 인스턴스들이 서로 상호작용을 함으로써, 객체 간의 관계 중심으로 프로그래밍 패러다임이 변화했다. 

(OOP, Object Oriented Programming)

 

포인터 메서드와 값 타입 메서드

리시버를 일반 값 타입과 포인터 값으로 정의할 수 있다. 

 

package main

import "fmt"

type account struct {
	balance int
	firstName string
	lastName string
}

// 포인터 메서드
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{
	a3.balance -= amount
	return a3
}

func main() {
	var mainA *account = &account{ 100, "Joe", "Park"}
	mainA.withdrawPointer(30) // 포인터 메서드 호출, mainA의 주솟 값이 a1으로 복사됨 
    // mainA와 a1은 같은 인스턴스를 바라봄
	fmt.Println(mainA.balance) // 70 출력

	mainA.withdrawValue(20) // 값 메서드 호출, mainA의 모든 값이 a2로 전달됨 
    // mainA와 a2는 서로 다른 인스턴스를 바라봄
	fmt.Println(mainA.balance) // 70 출력

	var mainB account = mainA.withdrawReturnValue(20) // mainB는 50
	fmt.Println(mainB.balance) // 값 메서드 값에서 변경된 값 반환 메서드 실행

	mainB.withdrawPointer(30) 
	fmt.Println(mainB.balance) // 20
}

 

메서드는 리시버의 타입을 그대로 따라가서 메서드는 리시버의 타입에 속함. 

포인터 메서드를 호출시, 포인터가 가리키는 메모리 주소값이 복사되고, 값 메소드 호출시, 리시버 타입의 모든 값이 복사됨. 

 

포인터 메서드는 메서드 내부의 리시버 값을 변경시킬 수 있음.

값 타입 메서드는 호출하는 쪽과, 메서드 내부의 값은 서로 다른 인스턴스이고, 따라서 메서드 내부 리시버 값을 변경시킬 수 없음

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 18장~19장을 읽고 공부한 글입니다. 

문자열

 

  • 문자열 표현법
  • UTF-8 구조
  • 문자열 동작 원리 

 

go언어에서는 UTF-8 문자코드를 사용함(한글이나 한자 사용에 문제 없음)

UTF-8문자 코드는 유니코드로써, 가변 길이 문자 인코딩 방식임

 

 

go언어에서 문자열은 큰따옴표나 백쿼드(`)로 묶어 표시한다. 

 

package main

import (
	"fmt"
)

func main() {
	str := "내 이름은 \t'김정자'\n"

	fmt.Println(str)

	str2 := `"야 내 이름은", 김 정 자 야. ^^`
    // 백쿼트로 문자열을 묶을 경우, 특수 문자가 동작하지 않음
	fmt.Println(str2)
}

// 큰 따옴표를 통해 여러 줄 표현시 \n을 사용해야함
// 백쿼트를 통해 여러 줄 표현시 아무것도 필요 없음

 

 

UTF-8문자 코드 

 

다국어 문자를 지원하고, 자주 사용되는 영문자나 숫자 등 일부 특수 문자를 1바이트로 표현함. 

(그 외의 다른 문자들은 2~3바이트로 표현)

ANSI 코드와 1:1대응이 가능하여, 바로 변환된다는 장점이 있음 

 

 

문자를 사용하는 경우 - rune 타입 사용

 

go에서는 문자 하나만 표현할 때 rune타입을 사용함

UTF-8 문자 값은 3바이트로 이루어져 있음, Go에서 문자 표현시 3바이트 정수 타입은 제공하지 않으므로, 4바이트 정수 타입인 int32타입의 별칭 타입인 rune타입을 사용해야함. 

 

package main

import (
	"fmt"
)

func main() {
	var chr rune = '야'
	// 사실 rune 타입은 int32타입과 같은 타입임
	fmt.Printf("%T\n", chr) // int32 출력
	fmt.Printf("%c", chr)   // 야 출력
	fmt.Println(chr)        // 50556 출력(chr 값)
}

 

 

len()으로 문자열이 차지하는 메모리 크기(문자열 크기를 확인)를  할 수 있음

 

package main

import (
	"fmt"
)

func main() {
	// 문자열은 아래와 같이 각 문자의 코드 값의 배열로 나타낼 수 있음 
	runes := []rune{72, 101, 108, 108}
	fmt.Println(string(runes)) // Hell

}

 

 

또한 string타입과 []byte 타입 간 상호 변환이 가능하다. 

 

문자열 순회는 byte단위로 한글자씩 순회하거나,

[]rune 타입으로 변환 후 한 글자씩 순회하거나, 반복문을 통해 순환할 수 있다. 

 

파이썬과 같이 문자열 연산은 +, += 연산자, 같은지 비교시 ==, !=를 사용할 수 있다. 

문자열에 대한 대소 비교시, >, <, >=, <=를 사용하고, 첫 글자부터 하나 씩 값 비교해서 유니코드 값을 비교함 

 

 

문자열 구조

 

type StringHeader struct {
	Data uintptr // 문자열의 데이터가 있는 메모리 주소를 나타냄 = 포인터
    Len int // 문자열의 길이 
}

 

만약 string끼리 대입할 경우, 대입 대상 변수에 기존 변수의 data, len값만 복사됨(= 문자열 자체는 복사되지 않음)

 

Go언어에서 string타입의 문자열의 전체는 변경 가능하지만, 일부만 변경할 수 없다. 

만약, string타입을 슬라이스 타입으로 타입 변경후(기존 문자열의 주소와는 다른 주소를 가리키는 상태로 변경), 일부 문자를 변경한 후 출력한다면 해당 문자열은 변경후 출력 가능함. 

 

문자열을 합산할 경우, 만약 문자열 a와 문자열 b가 있으면, 문자열 a와 b를 합친 문자열 c가 생성됨(a, b, c의 모든 메모리 주소는 서로 다른곳을 가리킴)

문자열 합산이 자주 일어날경우, 메모리가 낭비됨. 

이런 경우 strings패키지의 Builder을 이용하는 방법이 있음.

 

go에서 이렇게 하는 이유는, 버그를 방지하기 위함임 

 

패키지

  • 패키지 정의
  • 외부 패키지 사용하기
  • Go 모듈 알아보기 

 

패키지는 코드를 묶는 최대 단위임.

Go로 만든 모든 프로그램은 패키지를 묶어 만들게됨. 패키지를 통해 함수, 구조체, 상수 등을 외부에서 사용할 수 있음

 

함수를 통해 코드 블록을 묶고, 구조체로 데이터들을 묶고, 패키지를 통해 함수와 구조체 등 그외의 코드들을 묶음.

main 패키지는 다른 패키지들과는 다르게 프로그램의 시작점을 포함한 패키지임. 

 

Go의 프로그램은 main 패키지(필수)하나와 그 외 여러 외부 패키지(선택)으로 구성됨. 

 

 

그 외 여러 외부 패키지는 외부에서 가져다 쓰는 라이브러리와 같다. 

 

패키지 찾는 곳

https://golang.org/pkg/ 

 

Standard library - Go Packages

Discover Packages Standard library Version: go1.22.1 Opens a new window with list of versions in this module. Published: Mar 5, 2024 License: BSD-3-Clause Opens a new window with license information. Jump to ... Directories Directories ¶ Show internal Exp

pkg.go.dev

위 사이트를 통해 표준 패키지 목록 확인이 가능함

 

Go언어에서 많이 사용되는 패키지는 Awesome Go에서 찾아보면 좋다. 

https://github.com/avelino/awesome-go 

 

GitHub - avelino/awesome-go: A curated list of awesome Go frameworks, libraries and software

A curated list of awesome Go frameworks, libraries and software - avelino/awesome-go

github.com

 

 

패키지 사용하기 

 

// import 예약어로 임포트 하고, 원하는 패키지 경로를 따옴표로 묶어 써줌
import "fmt"

 

import {
	"fmt"
    "os"
}

 

이런식으로 다른 패키지를 가져오면 가져온 패키지에서 외부로 노출하는 함수나 구조체, 변수, 상수 등을 사용할 수 있음

 

외부 노출 여부는 이름의 첫 글자가 대문자면 노출한다는 의미이다. 

패키지 명은 가져오는 패키지 경로의 가장 마지막 폴더이름이다. 

 

fmt.Println("hi")

 

패키지를 통한 호출은 .을 통해 할 수 있다. 

 

package main

import ( // 둘이상의 패키지는 소괄호로 묶어줌
	"fmt"
	"math/rand" // 경로가 있는 패키지 사용
)

func main() { 
	fmt.Println(rand.Int()) // 경로가 있는 패키지 접근시 마지막 폴더명만 사용함
}

 

 

패키지 명이 겹칠경우, 별칭을 주어 구분한다. 

 

import {
	"text/template" // template 패키지
    "html/template" // 역시 같은 이름의 template 패키지
}

 

별칭을 쓸 경우 패키지명 앞에 사용

 

import (
	"text/template" // template 패키지
    htemplate "html/template" // 별칭 htemplate, 해당 패키지는 htemplate로 사용 가능함
)

 

사용하지 않는 패키지를 포함해야 하는 경우, 패키지 앞에 _ 을 붙여준다. 

 

 

import로 패키지를 포함시킬 경우, go build를 통해 파일을 빌드할 때, 해당 패키지를 찾아서 포함한 후 실행 파일을 생성함

해당 패키지는 다음과 같은 경로에서 확인할 수 있다. 

 

1. Go 설치 경로

2. GOPATH\pkg

3. 현재 폴더 아래의 위치

 

Go 모듈

Go 모듈은 Go 패키지들을 모아놓은 Go 프로젝트 단위이다. 

Go 1.16 버전부터 Go모듈 사용이 기본이 됨.

이전까지의 Go 코드는 GOPATH/src 폴더 아래에 있어야 했지만, Go 모듈이 기본이 되면서 모든 Go 코드는 Go모듈 아래에 위치해야

 

go build를 위해서, Go 모듈 루트 폴더에 go.mod파일이 있어야함.

go.mod 파일에는 모듈 이름, Go 버전, 필요한 외부 패키지 등이 명시되어 있음 

 

Go언어에서 go build를 통해 실행 파일을 만들 때, 

 go.mod와 go.sum(외부 저장소 패키지 버전 정보를 담고있음) 파일을 통해 외부 패키지와 모듈 내 패키지를 합쳐서 만듦

 

go mod init [패키지명]

 

위 명령어 실행시 go.mod파일이 생성됨 

 

히히 실습 완료

 

 

패키지를 만들때

Go언어에서 패키지명을 만들때, 쉽고 간결하게 이름 짓기를 권장한다. 

모든 문자를 소문자로 할 것을 권장한다. 

 

패키지 초기화

패키지를 임포트 할 때, 컴파일러는 패키지 내 전역변수를 초기화 한다. 그 후, 패키지에 init()함수가 있다면 호출해서 패키지를 초기화 한다. init()함수는 입력 매개변수가 없고, 반환값이 없는 함수여야 한다. 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 15장~17장을 읽고 공부한 글입니다. 

 

해당 글은 MacOS(M1) 환경을 기준으로 작성되었음. 

배열

C에서 사용하는 배열개념이랑 같다. 

배열에 담을 수 있는 요소는 처음에 초기화 한후에 고정됨(파이썬 리스트로 구현한 배열이랑 다름)

 

Go에서 배열 선언

var 변수명 [요소 개수]타입

// 5개의 float64타입의 원소를 가진 t라는 이름을 가진 배열 선언
var t [5]int

 

 

이렇게 선언만 하고 별도의 초깃값을 짖어하지 않으면 각 요솟값은 int 타입의 기본값 0으로 초기화 됨 

 

예제 작성전에 참고해야 할 것 

 

더보기

Go 1.16 버전부터 Go 모듈 사용이 기본이 됐음. 

 

Go 1.16 버전 이전에는 Go 모듈을 만들지 않는 코드는 모두 $GOPATH/src 폴더 밑에 있어야 했지만,

Go 모듈 사용이 기본이 되면서 모든 Go코드는 Go 모듈 밑에 있어야 한다. 

 

따라서, 모든 예제를 빌드하기 전에 $go mod init 을 통해 Go 모듈을 만들어 줘야함. 

 

아래의 명령어를 통해 대상 파일(폴더)에 대한 Go 모듈을 만들어준다. 

$go mod init [파일위치/파일명]

 

아래의 명령어를 통해 실행 파일을 만들어준다.

$go build 

 

파일을 실행한다. 

$./[파일명]

 

직접적인 사용 방법 예시

 

1. 배열을 반복문을 통해서 출력하기 

package main

import "fmt"

func main() {
	var t [5]float64 = [5]float64{23.0, 25.9, 22.8, 24.9, 21.2}

	for i := 0; i < 5; i++ {
		fmt.Println(t[i])
	}
}

 

 

배열 변수 선언 및 초기화 

var nums [5]int

 

별도 초기값을 정하지 않음으로, 요소값은 int타입의 기본값인 0으로 초기화됨 

 

month := [3]string{"jan", "feb", "mar"}

 

 

배열의 일부만 초기화 할 경우, 나머지 값들은 해당 타입의 기본값으로 초기화됨

var temps [5]float64{11.1, 22.2}

 

 

배열 요소 개수 생략하는 방법은 다음과 같음

 

x := [...]int{10, 20, 30}

 

주의할점. 배열을 선언할 때 개수는 항상 상수를 사용해야함. 

 

배열은 인덱스를 통해 읽고 쓸 수 있음

 

range를 통한 배열 순회

package main

import "fmt"

func main() {
	var t [5]float64 = [5]float64{23.0, 25.9, 22.8, 24.9, 21.2}

// i는 인덱스 v는 값. 
// 만약 인덱스가 필요없는 경우 _를 이용해 인덱스를 무효화 할 수 있음
	for i , v := range t{
		fmt.Println(i, v)
	}
}

 

 

배열의 요소는 연속된 메모리로 되어있음 (차원이 중첩되어도 마찬가지임)

 

요소 위치 = 배열 시작 주소 + (인덱스 * 타입 크기)

 

 

배열 크기 = 타입크기 * 항목 개수

 

구조체

  • 구조체 선언
  • 구조체 변수 사용
  • 구조체 특징
type  타입명  struct {
	필드명 타입 
    ...
    필드명 타입
}

// type 키워드를 사용함으로서 새 사용자 정의 타입을 정의함 
// 타입의 종류 struct 
// 중괄호{ } 안에 해당 구조체에 속한 필드들을 적어줌 
// 각 필드는 필드명과 타입을 기입

 

예시

type Sandwitch struct {
	Bread string
    Source string
    Price  int
}

 

Sandwitch 타입 구조체 변수 선언 

 

var egg Sandwitch

 

 

각 필드에 대한 접근은 . 연산자를 통해 접근한다. 

 

package main

type Sandwitch struct {
	Price  float64
	Bread  string
	Source string
	Cnt    int
}

func main() {
	var eggSandwitch Sandwitch
	eggSandwitch.Bread = "Wheat"
	eggSandwitch.Price = 6400.1
	eggSandwitch.Source = "salt"
	eggSandwitch.Cnt = 1
}

 

구조체 변수 초기화 

 

초깃값을 생략하면 모든 필드가 기본값으로 초기화 됨

 

다음과 같이 모든 필드를 순서대로 초기화 할 수 있음

 

var sandwitch Sandwitch = Sandwitch{ 100.0, "flat", "salt", 1 }

 

만약 필드 일부만 초기화할 경우, 초기화 되지 않은 나머지 변수에는 기본값으로 초기화 됨

 

구조체를 포함하는 구조체

 

type Sandwitch struct {
	Price  float64
	Bread  string
	Source string
	Cnt    int
}

type VIPSandwitch struct {
	SandwitchInfo Sandwitch
	Gifts string
}

 

 

포함된 필드 이용시 . 하나를 통해 접근이 가능함

package main

import "fmt"

type Sandwitch struct {
	Price  float64
	Bread  string
	Source string
	Cnt    int
}

type VIPSandwitch struct {
	Sandwitch
	Gifts string
    Price float64 // 필드가 중복되는 경우 호출할 때, 
}

func main() {
	sandwitch := Sandwitch{6400.1, "Wheat", "salt", 1}
	vip := VIPSandwitch{
		Sandwitch {100.0, "flat", "oliveOil", 2},
		"Cookie",
	}

	fmt.Printf("샌드위치 %s", sandwitch.Bread)
	fmt.Printf("샌드위치 %s", vip.Bread)
    fmt.Printf("샌드위치 가격: %d", vip.Sandwitch.Price)
    fmt.Printf("vip 샌드위치 가격: %d", vip.Price)
    

}

 

구조체 크기

구조체 변수가 선언되면, 컴퓨터는 구조체 필드를 모두 담을 수 있는 구조체의 메모리 공간을 할당함

 

type Student struct{
	Age int
    Score float64
}

 

 

만약 위와같은 구조체 Student가 정의 되어 있다고 했을때, 

 

var student Student

 

 

int 타입 Age는 8바이트 , float타입 Score는 8바이트로 총 16바이트의 크기가 필요함

student는 16바이트의 크기를 가지는 구조체임 

 

구조체의 객체는 대입연산자만으로도 필드값을 복사할 수 있음

Go 내부에서는 필드 각각이 아닌 구조체 전체 필드가 한 번에 복사됨.

 

메모리 정렬

 

메모리 정렬은 데이터에 접근할 때, 접근 효율을 높이기 위해 메모리를 일정 크기로 정렬하는 것을 말한다. 

 

레지스터는 실제 연산에 사용되는 데이터가 저장되는 곳인데, 레지스터 크기가 4바이트면 32비트 컴퓨터고,

레지스터 크기가 8바이트면 64비트 컴퓨터이다. 

 

레지스터 크기가 8바이트란 말은, 연산 한번에 8바이트 크기를 연산할 수 있다는 것이다. 

따라서 데이터가 레지스터 크기과 같은 크기로 정렬돼 있으면, 데이터를 읽어올 때 효율적이다. 

 

만약 

type Student struct {
	Age int32 // 4바이트 
    Score float64 // 8바이트
}

var student Student

 

인 경우, 

student의 메모리 주소가 240번지이면, Age의 시작 주소 또한 240이 된다. 

이때 Age의 바로 뒤에 Score의 메모리 주소를 할당하면 시작은 244가 된다. 

244는 8의 배수가 아니므로 메모리 접근 시에 효율이 떨어진다. 

 

이 때문에, 프로그램 언어에서 Student 구조체 할당 시, Age와 Score을 4바이트 띄워서 할당한다. 

그러면 Score의 메모리 주소 시작은 248이 된다. 

 

이런 방법을 메모리 패딩이라고 한다. 

 

메모리 낭비를 줄이기 위해서는 8바이트 보다 작은 필드는 8바이트 크기를 고려해서 몰아서 배치하는 것이 효율적이다. 

 

메모리 공간이 작인 임베디드의 프로그램이라면 패딩을 고려하는게 좋다. 

 

프로그램에서 구조체는 관련 데이터를 묶어 응집도를 높이고 재사용성을 증가시킨다. 

 

*결합도는 모듈간 상호 의존 관계의 높은 정도를 말하고, 응집도는 모듈의 완성도를 말하며 모듈 내부에 모든 기능이 하나의 목적에 맞게 충실하게 모여있는지를 나타내는 용어이다. 

 

 

포인터

  • 포인터 정의
  • 포인터 사용법
  • 인스턴스 개념

 

포인터는 메모리주소를 값으로 갖는 타입이다.

float 타입 변수 b가 있을때, b는 메모리에 저장되어있고, 메모리 주소는 속성으로 갖는다. 

그리고 변수 b의 주소가 0x0010번지일 때, 이 주소값은 숫자다. 

이 값은 다른 변수의 값으로 사용될 수 있다.  남의 변수 주소를  값으로 가질 수 있는 변수를 포인터 변수라고 한다. 

 

포인터 변수는 주소값만 값으로 가질 수 있는 변수다.

 

p = &b 

// p 포인터 변수는 변수 b의 주소값을 값으로 갖도록 대입하고 있다. 
// 이런 상태를 포인터 변수 p가 변수 b를 가리킨다고 말할 수 있다. 
// 메모리 주소를 값으로 가지고, 메모리 주소 값을 통해 메모리 공간을 가리키는 타입을 포인터 타입이라고 한다.

 

 

포인터는 여러 포인터 변수가 하나의 메모리 공간을 가리킬 수 있다. 

포인터가 가리키는 (메모리 주소가 가리키는) 공간에 있는 값을 읽거나 수정할 수 있다. 

 

포인터 변수 선언

 

var p *int

// 위와 같이 포인터 변수는 가리키는 데이터 타입 앞에 *을 붙여 선언한다.

var p2 *Student
// Student 구조체를 가리키는 경우 포인터 변수 선언 방법은 위와 같다.

 

 

포인터 변수에 값을 넣기 위해서는?

 

var b int
var p *int
p = &b

// 3번 라인은 b의 메모리 주소를 p 포인터 변수에 대입하는 것이다.

 

포인터 변수 p를 통해 b의 값을 변경하는 방법은?

 

*p = 100

 

 

// 포인터 변수 값을 출력하기 위해서는 ?

fmt.Printf("나는 포인터 변수 P이며, 값은 %p", p)

// 포인터 변수가 가리키는 메모리 공간의 값을 출력하기 위해서는 ?

fmt.Printf("나는 포인터 변수 P이며, 내가 가리키는 메모리 공간의 값은? %d", *p)

 

 

포인터 변수값 비교

 

포인터가 같은 메모리 공간을 가리키는지 확인하기 위해서 == 연산자를 사용할 수 있다. 

 

만약 같으면 true를, 같지 않으면 false를 출력한다. 

 

포인터의 기본값 nil

 

포인터 변수값을 초기화하지 않으면 기본값은 nil이다. 

nil은 따지고보면, 0이다. 그러나 정확한 의미는 어떠한 메모리 공간(주소)도 가리키고 있지 않다는 의미이다. 

 

var p *int
if p !=nil {
	// 포인터가 유효한 메모리 공간을 가리키고 있지 않아요!
}

 

 

포인터를 쓰는 이유

 

변수를 대입하거나, 함수에 인자를 통해 값을 전달할 때는 항상 값을 복사하여 활용한다. 

값을 복사한다는 의미는 메모리가 이중으로 사용된다는 의미이다. 

즉, 메모리가 필요 이상으로 많이 쓰인다는 이야기이다. 또 큰 메모리 공간을 복사할 경우 성능 문제가 발생할 수 있다. 

추가로 기존 값의 변경을 원할경우, 값이 복사되어 활용되므로, 변경 사항을 반영할 수 없다. 

 

다음과 같은 상황은 포인터를 메모리 낭비 없이 적절하게 사용하는 사용하는 경우이다. 

 

package main 

import "fmt"

type Data struct {
	value int
    data [200]int
}

func ChangeData(arg *Data) {
	arg.value = 100
    arg.data[100] = 100
}

func main(){
	var data Data
    
    ChangeData(&data) // 이때, 데이터 변수 값이 아닌 변수의 메모리 주소만 인수로 전달된다. 
    // 메모리 주소는 8바이트 숫자 값이다. 따라서 8바이트만 복사된다. 
    fmt.Printf("value = %d\n", data.value)
    fmt.Printf("data[100] = %d\n", data.data[100])
}

// 출력 결과 
// value = 999
// data[100] = 999

 

 

Data 구조체를 생성해 포인터 변수 초기화해보기 

 

구조체 변수를 따로 생성하여 포인터 변수에 구조체 변수를 가리키게 하는 방법

 

var data Data
var p *Data = &data

 

 

구조체 변수를 따로 생성하지 않고, 바로 포인터 변수에 구조체를 생성해 주소를 초기값으로 대입하는 방법은 다음과 같다. 

 

var p *Data = &Data{} // Data구조체를 생성해 초기화하는 방식
// &Data{} 는 데이터 구조체를 만들어 주소를 반환하는 것이다.

 

이 부분에서 이해가 안되겠지만, 일단 인스턴스로 넘어가길 권장한다. 

 

인스턴스

 

인스턴스는 메모리에 할당된 데이터의 실체이다.

var data Data

 

위는 Data 타입 값을 저장할 수 있는 메모리 공간(data)을 할당한다. 

 

이 상태에서 데이터 타입의 포인터 변수를 성넌하여 data 변수의 주소 값을 대입시키면 다음과 같다. 

var data Data
var p *Data = &data

 

 

이를 통해 p가 생성될 때, 새로운 Data의 인스턴스가 생성됐다고 보지 않고 기존의 data 인스턴스를 가리킨다고 본다. 

즉, 위 코드에서 Data 인스턴스의 총 개수는 한 개이다.

 

 

인스턴스를 따로 생성하지 않고, 바로 인스턴스 생성과 동시에 그 주소를 포인터 변수에 초기 값으로 대입하는 코드는 다음과 같다. 

 

var p *Data = &Data{}

 

이러면 Data인스턴스가 만들어지고, 해당 주소를 포인터 변수 p가 가리키게 된다. 

 

 

만약 다음과 같은 경우, 

 

var p1 *Data = &Data{}
var p2 *Data = p1
var p3 *Data = p1

 

포인터 변수가 아무리 많아도, p2, p3도 p1의 포인터변수 값을 갖는다.

따라서 p1, p2, p3 모두 공통된 인스턴스를 가리키게 된다. 즉, 포인터 변수가 많다 해도 인스턴스가 추가로 생성되지 않는다. 

 

 

인스턴스는 데이터의 실체

 

포인터를 이용해 인스턴스에 접근할 수 있다. 

구조체 포인터를 함수 매개변수로 받는다는 것은 구조체 인스턴스로 입력을 받겠다는 것이다. 

 

new() 내장 함수

 

포인터 값에 대해 따로 변수 선언을 하지 않고, 초기화 하는 또 다른 방법이 있다. 

 

p1 := &Data{} // 기존의 &를 사용한 초기화 
var p2 = new(Data) // new를 통한 초기화

 

 

인스턴스가 사라지는 타이밍

 

메모리는 한정되어 있으므로, 데이터가 할당되면 적절한 타이밍에 사라져야 프로그램이 계속 동작할 수 있다. 

Go에서는 데이터 할당 후 적절한 타이밍에 사라지게 하는 기능이 있다. 이를 가비지 컬렉터(메모리 청소부)라고 한다. 

이 기능이 일정 주기로 메모리에서 필요없는 데이터를 정리한다. 

 

Go에서는 아무도 찾지 않는 데이터를 사용하지 않는 데이터라고 인식하여 처리한다. 

 

예를들면 다음과 같은 상황이 있다. 

 

func TmpFunc(){
	s := &Student{} // s포인터 변수가 선언되고, Student인스턴스도 생성된다. 
    s.Age = 1
    fmt.Println(u)
}

// 이후 s는 사라지고, Student 인스턴스도 사라진다.

 

 

메모리는 엄청 크기 때문에, 가비지 컬렉터가 한번 움직일때는 큰 메모리를 전부 검사해서 쓸모 없어진 데이터를 정리하는데 성능이 많이 필요하다. 따라서, 가비지 컬렉터 사용시 메모리 관리는 잘 될지 몰라도 성능은 떨어질 수 있다. 

 

Go에서의 스택 메모리 힙 메모리 

 

대체로 프로그래밍 언어에서 메모리를 할당 할 때, 스택 메모리 영역이나 힙 메모리 영역을 사용한다. 

효율적인 면에서는 스택 메모리 영역이 힙 메모리 영역보다 우세하다. 

스택 메모리 영역에서 메모리를 할당할 경우, 스택 메모리는 함수 내부에서만 사용가능하다. 

 

C/C++에서는 malloc()함수를 직접 호출해 힙 메모리 공간을 할당하고,

Java에서는 기본 타입은 스택에, 클래스 타입은 힙에 할당한다. 

 

 

Go의 경우, 탈출 검사를 통해 어느 메모리에 할당할지 결정한다. 

함수 외부로 노출되는 인스턴스는 함수가 종료돼도 사라지지 않는다. 

함수 외부로 노출되는지 마는지의 여부는 Go에서 자동으로 검사해서 스택에 할당할지 힙에 할당할지 결정한다. 

 

Go에서 스택 메모리는 계속 증가되는 동적 메모리 풀이다. 

C/C++ 처럼 일정 크기를 갖지지 않아서 메모리 효율이 좋고, 재귀 호출로 인해 스택 메모리가 고갈되거나 하는 문제도 발생하지 않는다. 

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 12장~14장을 읽고 공부한 글입니다. 

 

상수

 

상수는 변하지 않는 고정 값이다 .

상수는 자주 쓰는 고정값에 이름을 정해줘서 쉽게 사용할 수 있게 한다. 

상수를 코드값으로 사용할 수 있으며, iota를 이용해 증가하는 상수를 쉽게 선언할 수 있다. 

상수를 타입 없이 선언할 경우, 타입이 다른 여러 변수에서 해당 상수를 사용할 수 있다. 

 

 

if문(조건문)

Go에서 사용하는 조건문은 C에서 사용하는 조건문과 사용방법이 비슷하다. 

 

단 한가지 다른 점이 있다.

C에서 do while 문과 비슷한 조건문을 사용하는 방법이 하나 존재한다. 

 

if 초기문; 조건문 {
	. . .
    // 초기문 먼저 실행 후 조건을 검사함
    
}

 



Switch문

 

Go의 Switch문은 파이썬에서 Switch문과 사용법이 비슷하다. 

기본 형태는 다음과 같다. 

 

switch 비교값 {
case value1:
 // 비교값이 value1과 같을 경우 실행 
 . . .
case value2:
 // 비교값이 value2과 같을 경우 실행 
 . . .
default:
 . . .
 // 만족하는 비교값이 없는 경우 실행하며,default문의 경우 생략 가능 
}

 

 

Go 에서 Switch 문은 비교값을 기입하지 않을 경우, 컴파일러는 항상 true로 인식한다. 

 

if문과 같이 switch문에도 초기문을 선언하여 먼저 실행시킨 후, 비교값을 확인하게 하는 방법이 존재한다. 

 

 

switch 초기문; 비교값 {
case value1
	. . .
    
}

 

 

For 문

 

Go에서는 for문을 C에 비해서 거의 자유자재로 사용할 수 있다. 

C언어와 같이 for문은 조건이 참이면 코드 반복을 수행하며, continue, break를 사용한다. 

 

Go에서 For문은 C에서 for문 선언하듯이 같고, 

 

초기문을 생략하거나

초기문 혹은 후처리를 생략하거나

초기문, 조건문, 후처리문 모두 생략할 수 있다. 이런경우 무한반복하게된다. 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 8장~11장을 읽고 공부한 글입니다. 

 

Go언어란?

홈페이지 주소: golang.org

온라인 Go 언어 컴파일러 : play.golang.org

  • Go 언어의 역사
  • Go 언어의 특징
  • Go 언어의 쓰임새

 

Go언어는 구글에서 로버트 그리스머, 롭 파이크, 켄 톰슨이 이끌어 만들어  2009년 발표된 오픈소스 프로그래밍 언어이다. 

즉, 무료이며, 언어 발전에 기여 가능하다. 

해당 언어는 심플하며, 성능이 좋다. 

 

해당 언어는 범용 언어로, 다양한 용도로 사용되지만

특히 백엔드 서버와 시스템 개발에 적합하며, 강력한 동시성 프로그래밍을 지원함

그 이유는 성능이 좋기 때문이다. 

 

특징 요약

 

  • 클래스: 없음, 그러나 메서드를 가지는 구조체를 지원함
  • 상속: 없음
  • 메서드: 있음, 구조체는 메서드를 가질 수 있음
  • 인터페이스: 있음, 상속은 없음
  • 익명 함수: 있음, 함수 리터럴이라는 이름으로 제공함
  • 가비지 컬렉터: 있음, 고성능 가비지 컬렉터를 제공함
  • 포인터: 있음
  • 제네릭 프로그래밍: 없음
  • 네임스페이스: 없음 

 

코드 실행 과정

 

  1. 폴더 생성
  2. .go파일 생성 및 작성
  3. Go 모듈 생성
  4. 빌드
  5. 실행 

 

폴더 생성

Go 언어에서 모든 코드는 패키지 단위로 작성됨.

같은 폴더에 위치한 .go 파일은 모두 같은 패키지에 포함되며, 폴더명이 패키지명이 됨.

 

예를들어서, goproject/hi/plus

 

hi 폴더에 든 .go 파일은 hi 패키지에 속하고, plus 폴더에 든 .go파일은 plus 패키지에 속함

 

Go 코드는 .go 확장자의 파일임 

Go 모듈 생성

 

Go 1.16 버전 이후로 Go 모듈이 기본으로 적용된다. 

따라서 모든 Go 코드 빌드 전에 모듈 생성이 필요하다. 

 

이때, 모듈 생성은 아래의 명령으로 실행한다. 

$ go mod init 모듈명

# 예를 들면

$go mod init goproject/hi

 

모듈 생성시, go.mod 파일이 생성되고, 해당 파일에는 모듈명, Go 버전, 필요한 패키지 목록에 대한 정보가 담겨있다. 

 

빌드

 

$ go build

 

위 명령어를 통해 Go 코드를 기계어로 변환하여 실행 파일을 만든다. 

 

GOOS와 GOARCH 환경변수 조정을 통해 다른 운영체제 및 아키텍처에서도 호환 가능한 실행 파일 제작이 가능하다. 

(터미널에서 $ go tool dist list 명령을 통해 호환 가능한 운영체제 및 아키텍처 목록을 확인할 수 있다. )

 

 

기본 코드에 대한 구조

 

package main 
// 패키지 선언: 해당 코드가 어떤 패키지에 속하는지 알려줌 
// 패키지는 코드의 묶음이며, 여러 기능을 제공함
// Go 파일의 코드는 항상 패키지 선언으로 시작해야함
// 1번 라인은 main 패키지에 속한 코드임을 컴파일러에 알려주는 역할을 함

// main 패키지는 프로그램 시작점을 포함하는 중요한 패키지임

import "fmt"
// fmt 패키지를 가져오겠다는 의미를 가짐
// fmt는 패키지 표준 입출력을 다루는 내장 패키지임

func main(){
	fmt.Println("Hi World")
	// fmt.println()는 표준 출력으로 문자열을 출력하는 함수임.
    // 표준 출력이란 터미널 화면을 말함. 
    // Go언어 프로그램은 main()함수 부터 시작되고, main() 함수가 종료되면 프로그램이 종료됨.
}

// Go 언어에서는 외부로 노출되는 함수 앞에는 함수명으로 시작하는 주석을 달아서 함수를 설명하는 것을 
// 코딩 규약으로 권장함

/* 여러줄 

주석*/

 

 

 

 

변수

 

  • 변수 선언
  • 변수 속성
  • 타입 변환
  • 숫자 표현

 

 

변수란 값을 저장하기 위한  메모리 공간을 가리키는 것이다. 

변수는 이름, 값, 타입, 주소 속성을 가지며, 변수 간 값의 전달은 항상 복사로 일어난다. 

 

컴퓨터에서 프로그램의 코드는 메모리의 데이터를 언제 변경할지 나타낸 문서이다. 

따라서, 프로그래밍은 메모리에 있는 데이터 조작이 핵심이다. 

이를 변수를 이용하면 쉽게 할 수 있다. 

 

변수 사용을 위해 아래와 같이 선언할 수 있다. 

코드에서 변수를 선언을 한다는 것은 컴퓨터에서 코드 실행시 변수를 위한 메모리 공간을 할당한다는 의미를 내포한다. 

따라서, 변수는 이름, 값, 타입, 주소를 속성으로 가진다. 

 

package main

import "fmt"

func main(){
	var tmp int = 11

}

 

위 코드와 달리 쉽게 변수 선언을 하기 위해 초깃값을 생략하거나 변수 타입을 생략하는 등 다양한 선언이 가능하다. 

 

변수의 타입은 처음 변환한 타입에서 변환할 수 있다. Go언어는 다른 언어들과 달리 타입에 대한 자동 변환을 지원하지 않는다. 

따라서 연산하거나 대입을 위한 타입 변환을 수동적으로 해줘야한다. 

 

변수 타입 중 숫자 타입은 변수 크기에 따라서 표현할 수 있는 값의 범위가 다르다. 

실수 타입은 유효 자릿수가 정해져 있으므로, 잘 확인하고 변수를 사용해야한다. 

 

텍스트 입출력 패키지 fmt

 

제목과 같이 Go에서는 fmt 패키지를 통해 데이터 표준 입출력을 할 수 있다. 

이때, 표준 입출력이란 터미널에서 데이터를 출력하고 입력하는 것을 말한다. 

 

표준 출력 함수로 Print(), Printf(), Println()을 사용할 수 있다. 

표준 입력 함수로 Scan(), Scanf(), Scanln()을 사용할 수 있다. 

 

변수를 출력할 때, 서식 문자를 활용해 최소 출력 너비 및 소숫점 이하로 몇개의 숫자까지 표현하는지 등 다양한 형식으로 출력할 수 있다. 

 

서식 문자 %v를 사용할 경우, 모든 타입의 기본 서식으로 출력할 수 있다. 

 

입력받을 때, 에러 발생시 표준 입력 스트림을 지우는 것이 좋다. 

 

 

 

연산자

 

C와 같이 산술 연산자로 사칙 연산, 비트 연산, 시프트 연산자가 있다. 

 

만약 정수 타입 숫자에서 경계 값에서 연산할 경우 주의가 필요하다. 

 

실수 타입에 대해 값의 같음을 확인하는 == 연산자 사용시, 정상적으로 동작하지 않을 수 있다. 

 

복합 대입 연산자를 통해 연산을 간편하게 줄여 사용할 수 있다. 

 

 

함수

 

함수는 한편으로는 코드 블럭 단위이다. 

 

함수를 사용하면 코드 재사용을 용이하게 할 수 있다. 

 

반환값이 여러개인 함수를 멀티 반환 함수라 하며, 함수 선언할 때, 반환 타입을 소괄호로 묶어 표시한다. 

 

함수는 다음과 같이 정의할 수 있다. 

 

func Sub(a int, b int) int {
	return a - b
}

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 3장~7장을 읽고 공부한 글입니다. 

 

 

+ Recent posts