본문 바로가기

Go

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

12장 배열

  • 배열은 같은 타입의 데이터들로 이루어진 저장소이다. 배열을 이루는 각 값은 요소라고 하고, 요소를 가리키는 위치값을 인덱스라고 한다. 

  • 아래의 소스는 배열을 사용한 간단한 예제이다.
//ch12/ex12.1/ex12.1.go
package main

import "fmt"

func main() {
	var t [5]float64 = [5]float64{24.0, 25.9, 27.8, 26.9, 26.2} // ❶

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

 

 

 

 

 

  • 배열은 선언시 개수는 항상 상수여야한다. 아래의 소스에서 a배열선언문은 오류가 발생한다. 배열의 길이를 지정할때 상수가 아닌값으로 지정하였기때문이다.
//ch12/ex12.2/ex12.2.go
package main

const Y int = 3 // ❶ 상수

func main() {
	x := 5                     // ❷ 변수
	a := [x]int{1, 2, 3, 4, 5} // ❸

	b := [Y]int{1, 2, 3} // ➍

	var c [10]int // ➎
}

 

 

 

  • 아래소스는 for문을 사용하여, 배열을 순회하는 예제이다. len()을 사용하여 배열의 길이를 구해서 해당 길이만큼 순회하여, 각각의 배열요소를 출력하는 소스이다.
//ch12/ex12.3/ex12.3.go
package main

import "fmt"

func main() {
	nums := [...]int{10, 20, 30, 40, 50} // ❶

	nums[2] = 300 // ❷

	for i := 0; i < len(nums); i++ { // ❸
		fmt.Println(nums[i]) // ➍
	}
}

 

 

 

  • for문에서 range라는 키워드를 사용하여, 위와는 다른 방법으로 배열을 순회할수있다.  i는 배열의 인덱스 , v는 해당 배열의 요소값이다.  인덱스값이 필요없을경우 _ 로 교체가능하다. (i,v를 선언하고 i를 사용안하면 오류가발생한다.)
//ch12/ex12.4/ex12.4.go
package main

import "fmt"

func main() {
	var t [5]float64 = [5]float64{24.0, 25.9, 27.8, 26.9, 26.2} // ❶

	for i, v := range t { // ❷
		fmt.Println(i, v) // ❸
	}
}

// --------------------------
// ch12/ex12.1/ex12.1.2.go

 

 

// ch12/ex12.1/ex12.1.2.go
package main

import "fmt"

func main() {
	t := [...]float64{24.0, 25.9, 27.8, 26.9, 26.2} // ❶

	for i := 0; i < len(t); i++ { // ❷
		fmt.Println(t[i]) // ❸
	}

	for i, v := range t {
		fmt.Println(i, v, t[i])
	}
}

 

 

 

 

  • 아래소스는 배열을 복사하는 소스이다. b = a 문장만으로 a의 모든 값을 b에 복사할수있다.  배열복사는 같은 타입, 같은 크기만이 복사할수있다. 사이즈가 다르거나 배열의 변수타입이 다르면 복사할때 오류가 발생한다.
//ch12/ex12.5/ex12.5.go
package main

import "fmt"

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	b := [5]int{500, 400, 300, 200, 100}

	for i, v := range a { // ❶ 배열 a 원소 출력
		fmt.Printf("a[%d] = %d\n", i, v)
	}

	fmt.Println()         // ❷ 개행
	for i, v := range b { // ❸ 배열 b 원소 출력
		fmt.Printf("b[%d] = %d\n", i, v)
	}

	b = a // ❹ a 배열을 b변수에 복사

	fmt.Println()         // 개행
	for i, v := range b { // ➎ 배열 b 원소 출력
		fmt.Printf("b[%d] = %d\n", i, v)
	}
}

 

 

 

  • 아래는 다중배열의 소스이다. 다중배열은 배열을 요소로 가지는 배열이다. 
//ch12/ex12.6/ex12.6.go
package main

import "fmt"

func main() {
	a := [2][5]int{ // ❶ 이중 배열 선언
		{1, 2, 3, 4, 5},
		{5, 6, 7, 8, 9}, // ❷ 여러 줄에 걸쳐 초기화할 때는 쉼표를 찍자!
	}
	for _, arr := range a { // ❸ arr값은 순서대로 a[0]의 배열 a[1]의 배열
		for _, v := range arr { // ❹ v값은 순서대로 a[0]과 a[1] 배열의 각 원소
			fmt.Print(v, " ") // ➎ v값 출력
		}
		fmt.Println()
	}
}

 

 

 

 

13장 구조체

 

  • 12장의 배열에서 보면 배열은 해당 배열의 요소들이 같은 타입이여야만한다. 구조체는 서로 다른 타입의 값들을 하나의 변수로 묶어줄수가 있다. 
  • 아래의 소스에서는 Address, Size, Price, Type을 House라는 이름의 구조체로 정의하였다.  문자열,정수, 실수등의 값을 하나의 값으로 묶었다. 
//ch13/ex13.1/ex13.1.go
package main

import "fmt"

type House struct { // ❶ House 구조체를 정의합니다.
	Address string
	Size    int
	Price   float64
	Type    string
}

func main() {
	var house House               // ❷ House 구조체 변수를 선언합니다.
	house.Address = "서울시 강동구 ..." // ❸ 각 필드값을 초기화합니다.
	house.Size = 28
	house.Price = 9.8
	house.Type = "아파트"

	fmt.Println("주소:", house.Address) // ❹ 필드값을 출력합니다.
	fmt.Printf("크기 %d평\n", house.Size)
	fmt.Printf("가격: %.2f억원\n", house.Price) // ➎ 소수점 2자리까지 출력합니다.
	fmt.Println("타입:", house.Type)
}

 

 

  • 구조체변수의 멤버를 구조체로 다시 지정할수있다. 아래의 소스에서 보면 User라는 이름의 구조체가 있는데, VIPUSER에서 멤버로 User를 사용하고 있다.
//ch13/ex13.2/ex13.2.go
package main

import "fmt"

type User struct { // 일반 고객용 구조체
	Name string
	ID   string
	Age  int
}

type VIPUser struct { // VIP 고객용 구조체
	UserInfo User
	VIPLevel int
	Price    int
}

func main() {
	user := User{"송하나", "hana", 23}
	vip := VIPUser{
		User{"화랑", "hwarang", 40},
		3,
		250, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
	} // ❶ User를 포함한 VIPUser 구조체 변수를 초기화합니다.

	fmt.Printf("유저: %s ID: %s 나이 %d\n", user.Name, user.ID, user.Age)
	fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d VIP 가격: %d만원\n",
		vip.UserInfo.Name, // ❷ UserInfo 안의 Name
		vip.UserInfo.ID,   // ❸ UserInfo 안의 ID
		vip.UserInfo.Age,
		vip.VIPLevel, // ❹ VIPUser의 VIPLevel
		vip.Price,    // ➎ 마지막에 쉼표
	)
}

 

 

 

  • 위의소스에서 VIPUser구조체로 vip를 생성하였는데, Name에 접근하려면 vip.UserInfo.Name 이렇게 두단계를 거쳐서 접근해야한다.
  • 하지만 아래 소스처럼 VIPUser구조체를 선언할때,  User필드명을 생략하는 경우 vip.Name이렇게 바로 접근이 가능하다.
//ch13/ex13.3/ex13.3.go
package main

import "fmt"

type User struct { // 일반 고객용 구조체
	Name string
	ID   string
	Age  int
}

type VIPUser struct { // VIP 고객용 구조체
	User     // ❶ 필드명 생략
	VIPLevel int
	Price    int
}

func main() {
	user := User{"송하나", "hana", 23}
	vip := VIPUser{
		User{"화랑", "hwarang", 40},
		3,
		250,
	}

	fmt.Printf("유저: %s ID: %s 나이 %d\n", user.Name, user.ID, user.Age)
	fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d VIP 가격: %d만원\n",
		vip.Name, // ❷ . 하나로 접근할 수 있습니다.
		vip.ID,
		vip.Age,
		vip.VIPLevel,
		vip.Price, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
	)
}

 

 

 

  • 만약에 상위구조체와 하위구조체의 필드명이 겹치면 어떻게 될것인가?
  • 아래의 소스를 보면 VIPUser구조체에 Level필드가 있고, 해당구조체의 하위구조체인 User구조체에도 Level필드가 있다.
  • 이럴경우 vip.Level은 VIPUser의 Level을 사용하게 된다. User구조체의 Level을 사용하려면 vip.User.Level이렇게 하면 사용할수있다. 
//ch13/ex13.4/ex13.4.go
package main

import "fmt"

type User struct {
	Name  string
	ID    string
	Age   int
	Level int // ❶ User의 Level 필드
}

type VIPUser struct {
	User  // ❷ Level 필드를 갖는 구조체
	Price int
	Level int // ❸ VIPUser의 Level 필드
}

func main() {
	user := User{"송하나", "hana", 23, 10}
	vip := VIPUser{
		User{"화랑", "hwarang", 40, 10},
		250,
		3, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
	}

	fmt.Printf("유저: %s ID: %s 나이 %d\n", user.Name, user.ID, user.Age)
	fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d 유저 레벨:%d\n",
		vip.Name,
		vip.ID,
		vip.Age,
		vip.Level,      // ➍ VIPUser의 Level
		vip.User.Level, // ➎ 포함된 구조체명을 쓰고 접근
	)
}

 

 

 

  • 구조체 값의 복사는 배열과 마찬가지로 대입연산자를 사용하여 복사할수있다.
//ch13/ex13.5/ex13.5.go
package main

import "fmt"

type Student struct {
	Age   int // ❶ 대문자로 시작하는 필드는 외부로 공개됩니다.
	No    int
	Score float64
}

func PrintStudent(s Student) {
	fmt.Printf("나이:%d 번호:%d 점수:%.2f\n", s.Age, s.No, s.Score)
}

func main() {
	var student = Student{15, 23, 88.2}

	// ❷ student 구조체 모든 필드가 student2 로 복사됩니다.
	student2 := student

	PrintStudent(student2) // ❸ 함수 호출시에도 구조체가 복사됩니다.
}

 

 

  • 아래의 소스를 보면 구조체의 각 요소의 크기는 4바이트,8바이트이다. 사이즈를 출력하면 12가 될것같지만 실제로는 16이 출력된다. 레지스터 크기가 8바이트인 컴퓨터는 한번의 연산에 8바이트 크기를 연산할수있다. 그래서 데이터 크기와 똑같은 크기로 정렬되어있으면 효율적으로 데이터를 읽어올수있다.
  • 아래의 경우에 Age는 4바이트이기때문에 8바이트로 정렬된다. 그래서 값이 16으로 출력된다.
//ch13/ex13.6/ex13.6.go
package main

import (
	"fmt"
	"unsafe"
)

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

func main() {
	user := User{23, 77.2}
	fmt.Println(unsafe.Sizeof(user))
}

 

  • 아래의 경우에는 사이즈가 19바이트이지만 실제로 출력하면, 40바이트가 출력된다. 메모리낭비가 21바이트나 낭비된다.
//ch13/ex13.7/ex13.7.go
package main

import (
	"fmt"
	"unsafe"
)

type User struct {
	A int8 // 1바이트
	B int  // 8바이트
	C int8 // 1바이트
	D int  // 8바이트
	E int8 // 1바이트
}

func main() {
	user := User{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(user))
}

 

  • 아래와 같이 1바이트짜리 변수들을 한군데로 몰아서 코드를 작성하면 24바이트의 메모리만 사용하게 된다.
//ch13/ex13.8/ex13.8.go
package main

import (
	"fmt"
	"unsafe"
)

type User struct {
	A int8 // 1바이트
	C int8 // 1바이트
	E int8 // 1바이트
	B int  // 8바이트
	D int  // 8바이트
}

func main() {
	user := User{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(user))
}

 

 

 

 

14장 포인터

  • 포인터는 메모리 주소를 값으로 갖는 타입이다.
  • 아래 그림과 같이 어떤변수가 있을때 그 변수가 메모리에 저장되는데 저장된 위치도 숫자값이기때문에 해당 숫자값을 갖는 변수를 포인터 변수라고 한다.

  • 아래 소스에서 a라는 변수가 500의 값을 가지고 있고, p라는 포인터 변수는 a의 주소값을 가지고 있다.
  • *p = 100 의 명령을 사용하여 p가 가리키고 있는 메모리에 100을 저장한다. 따라서 a의 값을 출력하면 100이 출력된다.
  • var p *int에서 p의 기본값은 nil 값을 가진다.
//ch14/ex14.1/ex14.1.go
package main

import "fmt"

func main() {
	var a int = 500
	var p *int // ❶ int 포인터 변수 p 선언

	p = &a // ❷ a의 메모리 주소를 변수 p의 값으로 대입(복사)

	fmt.Printf("p의 값: %p\n", p)            // ❸ 메모리 주솟값 출력
	fmt.Printf("p가 가리키는 메모리의 값: %d\n", *p) // ❹ p가 가리키는 메모리의 값 출력
	*p = 100                               // ➎ p가 가리키는 메모리 공간의 값을 변경합니다.
	fmt.Printf("a의 값: %d\n", a)            // ➏ a값 변화 확인
}

 

 

  • 아래의 소스에서 p1,p2는 모두 a변수의 주소값을 가지고 있다. 그래서 p1과 p2는 서로 같은 값이다.
//ch14/ex14.2/ex14.2.go
package main

import "fmt"

func main() {
	var a int = 10
	var b int = 20

	var p1 *int = &a // ❶ p1은 a의 메모리 공간을 가리킵니다.
	var p2 *int = &a // ❷ p2는 a의 메모리 공간을 가리킵니다.
	var p3 *int = &b // ❸ p3는 b의 메모리 공간을 가리킵니다.

	fmt.Printf("p1 == p2 : %v\n", p1 == p2)
	fmt.Printf("p2 == p3 : %v\n", p2 == p3)
}

 

  • 위의 소스에서 a 값과 p1,p2의 값은 같은 주소를 공유한다. a를 변경하면 p1,p2의 값도 변경된다.

 

 

 

  • 아래 소스에서 보면 ChangeData()에서는 data를 받는데, 해당 함수가 호출할때마다 Data구조체가 생성되어 해당값이 복사되어 전달된다(Call by value). 그래서 함수내에서 어떤값을 바꾸더라도 main()에 있는값은 바뀌지 않는데, 해당 함수는 호출할때마다 Data구조체를 생성하는데 총 1608바이트가 새로 생성된다. 
  • 만약 해당 함수가 반복문에서 자주 호출되는 함수라면 메모리가 낭비되고, 성능이 저하될수있다.
//ch14/ex14.3/ex14.3.go
package main

import "fmt"

type Data struct { // ❶ Data형 구조체
	value int
	data  [200]int
}

func ChangeData(arg Data) { // ❷ 파라미터로 Data를 받습니다.
	arg.value = 999
	arg.data[100] = 999
}

func main() {
	var data Data

	ChangeData(data) // ❸ 인수로 data를 넣습니다.
	fmt.Printf("value = %d\n", data.value)
	fmt.Printf("data[100] = %d\n", data.data[100]) // ❹ data 필드 출력
}

 

 

  • 아래의 소스는 ChangeData() 함수에서 파라미터를 포인터 변수로 받는다.
  • 이렇게 받을경우 해당 함수를 호출할때마다 4바이트만 사용되기때문에, 여러번 호출해도 성능이 떨어지지 않는다.
  • 포인터변수로 받기때문에 해당 함수에서 값을 변경하면 main()함수에도 해당 변경된값이 영향을 받는다.
//ch14/ex14.4/ex14.4.go
package main

import "fmt"

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

func ChangeData(arg *Data) { // ❶ 파라미터로 Data 포인터를 받습니다.
	arg.value = 999 // ❸ arg 데이터 변경
	arg.data[100] = 999
}

func main() {
	var data Data

	ChangeData(&data)                      // ❷ 인수로 data의 주소를 넘깁니다.
	fmt.Printf("value = %d\n", data.value) // ❹ data 필드값 출력
	fmt.Printf("data[100] = %d\n", data.data[100])
}

 

 

  • 아래 소스에서 NewUser()함수에서 User를 생성하여 리턴한다. 해당 값은 사라지지 않는다.
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func NewUser(name string, age int) *User {
	var u = User{name, age}
	return &u // 1 탈출 분석으로 u 메모리가 사라지지 않음
}

func main() {
	userPointer := NewUser("AAA", 23)

	fmt.Println(userPointer)
}