0%

Go语言学习笔记1-基本语法

[TOC]

变量声明(使用var关键字)

变量(Variable)的功能是存储用户的数据,不同的逻辑有不同的对象类型,也就有不同的变量类型

Go语言使用var关键字进行变量的声明

1
2
3
4
5
6
7
var a int
var b string
var c []float32
var d func() bool
var e struct{
x int
}
  • 第一行,声明一个整形类型的变量,用来保存整数数值
  • 第二行,声明一个字符串类型的变量
  • 第三行,声明一个32位浮点切片类型的变量,浮点切片表示由多个浮点类型组成的数据结构
  • 第四行,声明一个返回值为bool类型的函数变量,这种形式一般用于回调函数,即将函数以变量的形式保存下来,在需要的时候重新调用这个函数
  • 声明一个结构体类型的变量,这个结构体拥有一个整形的x字段

标准格式声明

1
var 变量名 变量类型

变量的声明是以var关键字开头,要声明的变量名放中间,将其类型放在后面,行尾无需分号

批量格式声明

1
2
3
4
5
6
7
8
9
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)

使用var和括号,可以将一组变量定义放在一起

变量初始化

Go语言在声明变量时,自动对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如:

  • 整型和浮点型变量的默认值为 0
  • 字符串变量的默认值为空字符串
  • 布尔型变量默认为 bool
  • 切片、函数、指针变量的默认为 nil

在声明变量的时候也可以进行赋初始值

变量初始化标准格式

1
2
var 变量名 类型 = 表达式
var name string = "zhimma"

编译器推导类型

在标准格式的基础上,可以省略部分变量类型,编译器会尝试根据等号右边的表达式推导变量的类型

1
var name = "zhimma"

等号右边的部分在编译原理里被称做右值(rvalue)

短变量声明并初始化

var 的变量声明还有一种精简写法

1
name := "zhimma"

左值变量必须是没有定义过的变量

在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,代码如下:

1
2
conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

多个变量同时赋值

1
2
3
4
var a int = 100
var b int = 200

b, a = a, b

多重赋值时,变量的左值和右值按从左到右的顺序赋值
多重赋值在 Go 语言的错误处理和函数返回值中会大量地使用。

匿名变量

在使用多重赋值时,如果不需要在左值中接收变量,可以使用匿名变量(anonymous variable)

匿名变量的表现是一个下划线_,使用匿名变量时,只需要在变量声明的地方使用下画线替换即可

1
2
3
4
5
6
func GetData() (int, int) {
return 100, 200
}
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)// 100 200

匿名变量不占用命名空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

匿名变量:

  • 可以理解为一种占位符。
  • 本身这种变量不会进行空间分配,也不会占用一个变量的名字。
  • for range 可以对 key 使用匿名变量,也可以对 value 使用匿名变量。

Go语言类型

Go语言 中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异,切片类型有着指针的便利性,但比指针更为安全,很多高级语言都配有切片进行安全和高效率的内存操作。

整数类型

整型分为以下两个大类:

  • 按长度分为:int8、int16、int32、int64
  • 还有对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8 就是我们熟知的 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long 型。

浮点类型(小数类型)

Go语言

支持两种浮点型数:float32 和 float64。这两种浮点型数据格式遵循 IEEE 754 标准:

  • float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。
  • float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。

打印浮点数时,可以使用 fmt 包配合动词%f,代码如下:

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) //按默认宽度和精度输出整型
fmt.Printf("%.2f\n", math.Pi) //按默认宽度,2 位精度输出(小数点后的位数)。
}
// 3.141593
// 3.14

bool类型

布尔型数据只有 true(真)和 false(假)两个值,布尔型无法参与数值运算,也无法与其他类型进行转换

字符串

字符串在Go语言中以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

转移符 含 义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
' 单引号
" 双引号
\ 反斜杠

定义多行字符串

在源码中,将字符串的值以双引号书写的方式是字符串的常见表达方式,被称为字符串字面量(string literal)
这种双引号字面量不能跨行。如果需要在源码中嵌入一个多行字符串时,就必须使用**`**字符,代码如下:

1
2
3
4
const str = `第一行
第二行
第三行
`

在**`**间的所有代码均不会被编译器识别,而只是作为字符串的一部分

字符类型(byte和rune)

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符

Go语言的字符有以下两种:

  1. uint8类型,也叫byte型,代表了ASCLL码中的一个字符
  2. rune类型,代表一个UTF-8字符。当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32。

使用fmt.Printf中的%T动词可以输出、变量的实际类型,使用这个方法可以查看 byte 和 rune 的本来类型

1
2
3
4
5
var a byte = 'a'
fmt.Printf("%d %T\n", a, a) // 97 uint8

var b rune = '你'
fmt.Printf("%d %T\n", b, b) // 20320 int32

可以发现,byte 类型的 a 变量,实际类型是 uint8,其值为 ‘a’,对应的 ASCII 编码为 97
rune 类型的 b 变量的实际类型是 int32,对应的 Unicode 码就是 20320
Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

Go数据类型转换

Go语言使用类型前置加括号的方式进行数据类型转换,一般格式如下:

1
T(表达式)

T代表要转换的类型。表达式包括变量、复杂算子和函数返回值等

类型转换时,需要考虑两种类型的关系和范围,是否会发生数值截断等

常量

相对于变量,常量是恒定不变的值,例如圆周率。
可以在编译时,对常量表达式进行计算求值,并在运行期使用该计算结果,计算结果无法被修改。常量表示起来非常简单,如下面的代码:

1
const pi = 4.14159

常量的声明和变量声明非常类似,只是把 var 换成了 const。

多个变量可以一起声明,类似的,常量也是可以多个一起声明的,如下面的代码:

1
2
3
4
const (
pi = 3.141592
e = 2.718281
)

常量因为在编译期确定,所以可以用于数组声明,如下面的代码:

1
2
const size = 4
var arr [size]int

模拟枚举(const和iota模拟枚举)

Go语言现阶段没有枚举,可以使用 const 常量配合 iota 模拟枚举,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Weapon int
const (
Arrow Weapon = iota // 开始生成枚举值, 默认为0
Shuriken
SniperRifle
Rifle
Blower
)
// 输出所有枚举值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower) // 1 2 3 4
// 使用枚举类型并赋初值
var weapon Weapon = Blower
fmt.Println(weapon) // 4

枚举类型其实本质是一个 int 一样。当然,某些情况下,如果需要 int32 和 int64 的枚举,也是可以的。

Go语言type关键字(类型别名)

类型定义

1
type byte uint8

类型别名

类型别名的写法为:

1
type TypeAlias = Type

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"



// 将NewInt定义为int类型
type NewInt int

// 将int取一个别名叫IntAlias
type IntAlias = int

func main() {

var a NewInt
fmt.Printf("a type : %T\n", a) // a type : main.NewInt

var b IntAlias
fmt.Printf("b type %T\n", b) // b type int
}

代码说明如下:

  • 第 8 行,将 NewInt 定义为 int 类型,这是常见定义类型的方法,通过 type 关键字的定义,NewInt 会形成一种新的类型。NewInt 本身依然具备int的特性。
  • 第 11 行,将 IntAlias 设置为 int 的一个别名,使用 IntAlias 与 int 等效。
  • 第 15 行,将 a 声明为 NewInt 类型,此时若打印,则 a 的值为 0。
  • 第 16 行,使用%T格式化参数,显示 a 变量本身的类型。
  • 第 18 行,将 b 声明为 IntAlias 类型,此时打印 b 的值为 0。
  • 第 19 行,显示 b 变量的类型。

结果显示a的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型。b 类型是 int。IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。

Go语言指针

指针(pointer)概念在Go语言中被拆分为两个核心概念:

  1. 类型指针,运行对这个指针类型的数据进行修改,传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算
  2. 切片,由指向起始元素的原始指针、元素数量和容量组成

受益于这样的约束和拆分,Go 语言的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

要明白指针,需要知道几个概念:指针地址指针类型指针取值,下面将展开细说。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置

Go 语言中使用&作符放在变量前面对变量进行“取地址”操作。

1
ptr := &v // v的类型为T

其中v代表被取地址的变量,被取地址的v使用ptr变量进行接收,ptr的类型就为*T,称做 T 的指针类型,*代表指针。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)

func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str) // 0xc042052088 0xc0420461b0
}

代码说明:

  • 第 8 行,声明整型 cat 变量。
  • 第 9 行,声明字符串 str 变量。
  • 第 10 行,使用 fmt.Printf 的动词%p输出 cat 和 str 变量取地址后的指针值,指针值带有0x的十六进制前缀。

输出值在每次运行是不同的,代表 cat 和 str 两个变量在运行时的地址。

提示:变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址

指针取值

在对普通变量使用&操作符获取地址获得这个变量的指针后,可以对指针使用*操作,也就是指针取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
// 声明一个字符串类型变量
var house = "陕西西安"
// 对字符串取地址,ptr类型为*string
ptr := &house

// 输出ptr类型
fmt.Printf("ptr type: %T\n" , ptr) // ptr type: *string
// 输出ptr指针地址
fmt.Printf("address: %p\n" , ptr) // address: 0xc00000e1e0

// 对指针进行取值操作
value := *ptr
// 取值后类型
fmt.Printf("value type: %T\n" , value) // value type: string
// 指针去之后就是指向变量的值
fmt.Printf("value: %s\n" , value) //value: 陕西西安
}

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性

如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

使用指针修改值

通过指针不仅可以取值,也可以修改值。

前面已经使用多重赋值的方法进行数值交换,使用指针同样可以进行数值交换,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
// 准备两个变量, 赋值1和2
x, y := 1, 2
// 交换变量值
swap(&x, &y)
// 输出变量值
fmt.Println(x, y) // 2 1
}

func swap(a, b *int) {
// 取a指针的值, 赋给临时变量t
t := *a
// 取b指针的值, 赋给a指针指向的变量
*a = *b
// 将a指针的值赋给b指针指向的变量
*b = t
}

*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指向的变量

*操作符的根本意义就是操作指针指向的变量,当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

创建指针的另一种方法——new() 函数

Go 语言还提供了另外一种方法来创建指针变量,格式如下:

1
new(T) // T代表类型

一般这样写:

1
2
3
4
str := new(string)
*str = "zhimma"

fmt.Println(*str) // zhimma

new() 函数可以创建一个对应类型的指针,创建过程会分配内存。被创建的指针指向的值为默认值。