배열은 같은 타입의 데이터들로 이루어진 저장소이다. 배열을 이루는 각 값은 요소라고 하고, 요소를 가리키는 위치값을 인덱스라고 한다.
아래의 소스는 배열을 사용한 간단한 예제이다.
//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)
}