Golang基础(二):变量与数据类型
变量声明
golang变量以关键字var开头,变量名放中间,变量类型声明后置:
var 变量名 变量类型
1 | var name string |
为了简化掉var,还支持批量变量声明:
1
2
3
4
5
6var(
a string
b int
c bool
d float32
)
变量赋值
和C++类似,变量声明会进行默认初始化,例如整型和浮点型默认为0,字符串类型默认为空字符串,布尔型默认为false,切片/函数/指针变量默认为nil。
同样可以在声明时通过表达式赋值: 1
2var name string = "EdemMo"
var age int = 18
编译器还支持自动的类型推导,省略类型声明,甚至一个等式声明多种变量:
1
2
3
4var name = "Eden"
var age = 18
var name,age = "Eden", 18 //ok
函数变量
全局变量使用上述方式进行声明,如果是函数内部的局部变量,无需var也无需类型,直接使用:=赋值也可以(不能在函数外使用):
1
2
3
4
5
6
7
8
9package main
import "fmt"
var m = 100 //全局变量
func main() {
n := 10
m := 200 //使用局部变量
fmt.Println(m, n) //200 10
}
匿名变量
在多重赋值,使用下划线"_"代表匿名变量,既不占用命名空间,也不会为其分配内存,作占位符,表示忽略的值,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func foo() (int, string) {
return 18, "EdenMo"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x) //x= 18
fmt.Println("y=", y) //y= EdenMo
}
常量
同C一样,常量关键字为const,声明时必须赋值:
1
2const pi = 3.1415
const e = 2.71821
2
3
4
5
6
7
8
9
10const (
a = 0.9
b = 0.8
c
d
)
func main() {
//a,b,c,d = 0.9 0.8 0.8 0.8
fmt.Println("a,b,c,d =", a, b, c, d)
}1
2
3
4
5
6
7
8
9
10
11
12const (
a = iota // 0
b = iota // 1,
c // 2,等价于 c = iota
)
//新const块重新从0开始
const (
d = iota // 0
e // 1
f // 2
)1
2
3
4
5
6
7
8
9
10type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
也可以从1开始,也支持使用匿名变量跳过某些值(因为占位符不占据内存,因此是允许反复使用的):
1
2
3
4
5
6
7
8
9
10type Weekday int
const (
Monday Weekday = iota+1 // 1
Tuesday // 2
Wednesday // 3
_ // 4
Friday // 5
_ // 6
Sunday // 7
)
注意,注意,iota是按行计数,而不是按次计数,同一行的iota值是相同的:
1
2
3
4
5const (
a, b = iota, iota + 10 // a=0, b=10
c, d // c=1, d=11
e, f // e=2, f=12
)
const赋值不影响iota自身的计数递增,因此可以混合赋值:
1
2
3
4
5
6
7
8const (
a = iota // 0
b // 1
c = "hi" // "hi",iota 此时为 2
d // "hi",沿用上一行表达式,iota 为 3
e = iota // 4,重新使用 iota,值为当前行号
f // 5
)
也常用于表示存储递增: 1
2
3
4
5
6
7
8const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
数据类型
基本数据类型
整型:
uint8,uint16,uint32,uint64,int8,int16,int32,int64;特殊整型:
uint,int;32位系统对应uint32/int32,64位则为uint64/int64;uintptr:无符号整型,存放一个指针;
同C/C++一样,golang支持进制赋值,额外支持下划线分隔符:
1
2
3
4var c int = 0xff
fmt.Printf("%x \n", c) //格式化打印
v := 123_456 //123456浮点型:
float32、float64布尔:和C不同,golang的
bool不允许和其他类型相互转换;
字符串string、字节byte和字符rune
Go约定使用UTF-8编码存储字符串,string一旦创建不能被修改;
byte: uint8类型,表示一个字节,用于处理二进制原始数据和ASCII字符;
rune:int32类型,表示一个字符,即一个Unicode码点;
1 | package main |
反引号禁用转义
使用反引号定义的字符串自动忽略转义,被称为原生字符串字面量(Raw
string literal),常用于写正则、SQL、多行文本等: 1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func main() {
s := `Hello\t\nWorld`
fmt.Println(s) //Hello\t\nWorld
s_mult :=
`第一行
第二行
第三行`
fmt.Println(s_mult)
}1
2
3第一行
第二行
第三行
这是因为反引号间我们的代码缩进tab也被打印出来了,要对齐,必须诚实地去除一切缩进:
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func main() {
s := `Hello\t\nWorld`
fmt.Println(s) //Hello\t\nWorld
s_mult :=
`第一行
第二行
第三行`
fmt.Println(s_mult)
}1
2
3第一行
第二行
第三行
字符串不可变与转化修改
同C类似,字面常量禁止修改字符: 1
2s := "hello"
s[0] = 'H' //error!byte或者rune处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "fmt"
func main() {
s := "Hello"
b := []byte(s)
b[0] = 'h'
s1 := string(b)
fmt.Println(s1) //hello
ss := "中国"
r := []rune(ss)
r[0] = '美'
ss1 := string(r)
fmt.Println(ss1) //美国
}
字符串操作
拼接、比较、切片: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package main
import (
"fmt"
"strings"
)
func main() {
//简单拼接:
s := "Hello"
s = s + " World"
fmt.Println(s) //Hello World
//多次大量拼接建议使用strings.Builder
var bdstring strings.Builder
bdstring.WriteString("Hello")
bdstring.WriteString("World")
bdstring.WriteString("God")
s1 := bdstring.String()
fmt.Println(s1) //HelloWorldGod
//字符串比较
fmt.Println(s != s1) //true
//切片
s2 := s[0:5]
s3 := s1[0:5]
fmt.Println(s2 == s3) //true
//如果是Unicode切片,必须使用rune再切片
s4 := "你好"
r := []rune(s4)
s4Sub := string(r[1:2]) //2是下标,并非长度,左闭右开
fmt.Println(s4Sub)
}
其他辅助函数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package main
import (
"fmt"
"strings"
)
func main() {
//是否包含子串
s := "EdenMo"
isContain := strings.Contains(s, "Edex")
fmt.Println(isContain) //false
//是否以子串开头
isPrefix := strings.HasPrefix(s, "Eden")
fmt.Println(isPrefix) //true
//是否以子串结尾
isSuffix := strings.HasSuffix(s, "Mo")
fmt.Println(isSuffix) //true
//查找字串第一次出现下标
id := strings.Index(s, "d")
fmt.Println(id) //1
//转换大小写:
s1 := strings.ToUpper(s)
fmt.Println(s1) //EDENMO
s2 := strings.ToLower(s)
fmt.Println(s2) //edenmo
//替换子串:
// 最后的数字n: n>0,替换n次;n=0,不替换;n<0,有多少替换多少
s3 := strings.Replace(s, "Eden", "Mike", -1)
fmt.Print(s3) //MikeMo
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import (
"fmt"
"strings"
)
func main() {
s := "a,b,c,d"
parts := strings.Split(s, ",")
for _, ss := range parts {
fmt.Println(ss) //a,b,c,d
}
//或使用Go风格默认打印
fmt.Printf("%#v\n", parts) //[]string{"a", "b", "c", "d"}
}
虽然字符串存储是按字节存储的,因此含中文或者其他多编码文本时切片时不能直接按序号截断,但golang支持按Unicode编码进行分割:
1
2
3s := "GO世界和平"
parts := strings.Split(s, "")
fmt.Printf("%#v\n", parts) //[]string{"G", "O", "世", "界", "和", "平"}
按字符串分隔符重新拼接: 1
2
3parts := []string{"a", "b", "EdenMo"}
s := strings.Join(parts, ",")
fmt.Println(s) // "a,b,EdenMo"
复数
golang具有复数类型定义,分别是:
complex64: 实部和虚部都是32位;
complex128: 实部虚部均为64位;
1 | package main |
类型转换
golang只有强制类型转换:
T(表达式)
如: 1
2
3var a, b = 3, 4
c := int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c) //5
运算符
大部分场景同C/C++,为了方便,直接类比和C/C++的差异。
自增、自检独立一行
golang没有前缀++或者--,后缀++/--只能作独立一行,不能作表达式值:
1
2
3
4
5
6i := 10
i++
fmt.Println(i) //ok, 11
b := i-- +10 //error!
--i //error!
无三目运算符
只能使用条件赋值: 1
2
3
4
5
6
7var x int
if true{
x = a
}
else{
x = b
}
不支持指针算术
在C/C++中: 1
2int *p = arr;
p++; //ok1
2var p *int
p++ //error!
golang位移必须使用无符号整型或者非负常量
1 | var b int = 1 |
golang数据类型比较规则
在golang中,对operator==和operator!=:
golang不支持自定义重载运算符;
基本数据类型均能够直接比较;
slice、map、func不能互相比较,只能判断是否为nil(和nil比较);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package main
import "fmt"
func f1() int {
return 0
}
//无类型声明,即void
func f2() {}
func main() {
//slice只能与nil比较
var s1 []int
s2 := []int{1, 2, 3}
var s3 []int = nil
fmt.Println(s1 == nil && s3 == nil && s2 == nil)
//error!
//fmt.Println(s1 == s2)
//fmt.Println(s2 == s2)
//map也只能与nil比较:
m1 := map[string]int{"a": 1}
var m2 map[int]int //声明和定义分离
m2 = map[int]int{1: 2, 3: 4}
m2[3] = 5 //直接替代4
//make声明
m3 := make(map[int]int)
m3[0] = 1
fmt.Println(m1 == nil || m2 == nil || m3 == nil)
//func亦是只能与nil比较
fn1 := f1
var fn2 func()
var fn3 func() = nil
fmt.Println(fn1 == nil) //false
fmt.Println(fn2 == nil && fn3 == nil) //true
}array、struct中元素均可比较,那么也能直接比较,否则不能比较;对struct而言,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import "fmt"
type Person struct {
Name string
Age int
}
type User struct {
Name string
Tags []string //slice类型不可比较
}
func main() {
p1 := Person{"Eden", 18}
p2 := Person{"Mike", 18}
fmt.Println(p1 == p2) //ok, false
u1 := User{"A", []string{"Eden", "Mo"}}
u2 := User{"A", []string{"Eden", "Mo"}}
fmt.Println(u1 == u2) //error!不可比较!
}对array,能够嵌套讨论,含不能比较struct不能比较:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28package main
import "fmt"
type Person struct {
Name string
Age int
}
type User struct {
Name string
Tags []string //slice类型不可比较
}
func main() {
a1 := [3]int{1, 2, 3}
a2 := [3]int{1, 2, 3}
fmt.Println(a1 == a2) // ok, int array能够直接比较
a3 := [2]Person{{"Eden", 18}, {"Mo", 18}}
a4 := [2]Person{{"Eden", 18}, {"Mo", 18}}
fmt.Println(a3 == a4) //ok, Person array是合法的
a5 := [2]User{{"Eden", []string{"Eden", "Mo"}}, {"Mike", []string{"Mike", "Wong"}}}
a6 := [2]User{{"Eden", []string{"Eden", "Mo"}}, {"Mike", []string{"Mike", "Wong"}}}
fmt.Println(a5 == a6) //error! User struct array不能比较!
}
数组Array
golang的数组有以下鲜明的特点:
数组长度固定且必须为常量,一旦定义不可修改;
长度和元素类型同时构成一个数组的数据类型,
var a[5] int和var a[10]int是不同的数据类型;数组是值类型,直接赋值和传参会复制整个数组和创建副本,传递的是值而并非指针,而不是修改原来的值;
一维数组初始化
有若干种初始化方法,既可以自动推导长度,还可以按索引分配元素,其余元素保留默认值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var a1 = [5]int{1, 2, 3, 4, 5}
var a2 = [...]int{1, 2, 3, 4, 5} //可以自动推导长度
var a3 [5]int = [5]int{1, 2, 3, 4, 5}
var str = [5]string{3: "third", 4: "fourth"}
func main() {
la1 := [3]int{1, 2, 3}
la2 := [...]int{1, 2}
la3 := [...]string{3: "third", 4: "fourth"} //自动推导+指定序号,实际分配等效于4+1,即5
la4 := [...]struct {
name string
age int
}{
{"Eden", 18},
{"Mike", 18}, //就算后续没元素,这里也要加逗号
}
}
最常用数字遍历和range遍历方法,是等效的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14la4 := [...]struct {
name string
age int
}{
{"Eden", 18},
{"Mike", 18}, //就算后续没元素,这里也要加逗号
}
for i, elem := range la4 {
fmt.Println(i, elem)
}
for i := 0; i < len(la4); i++ {
fmt.Println(i, la4[i])
}
多维数组
多维数组只有最外层能够使用...进行数组长度推导,其他维度不支持推导:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package main
import "fmt"
func main() {
a1 := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
a2 := [...][2]int{
{1, 2},
{3, 4},
}
//二维遍历:
//0 0 1
//0 1 2
//0 2 3
//1 0 4
//1 1 5
//1 2 6
for i, v1 := range a1 {
for j, v2 := range v1 {
fmt.Println(i, j, v2)
}
}
fmt.Println("\n")
// 0 0 1
// 0 1 2
// 1 0 3
// 1 1 4
for i := 0; i < len(a2); i++ {
for j := 0; j < len(a2[i]); j++ {
fmt.Println(i, j, a2[i][j])
}
}
}
值传递、指针传递和切片传递
golang没有引用传递,要避免值传递的副本内存和复制开销,可以使用指针传递或者切片传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package main
import (
"fmt"
"runtime"
)
// 实现函数名打印函数
func getRunTimeInfo() (pc uintptr, file string, line int, ok bool) {
return runtime.Caller(1) //1代表调用者,0代表当前函数,2代表调用者的调用者,以此类推
}
// 值传递
func test(x [2]int) {
pc, file, line, _ := getRunTimeInfo()
fmt.Printf("[%s][line_%d][%s]: %p\n", file, line, runtime.FuncForPC(pc).Name(), &x)
}
// 指针传递
func testPtr(x *[2]int) {
pc, file, line, _ := getRunTimeInfo()
fmt.Printf("[%s][line_%d][%s]: %p\n", file, line, runtime.FuncForPC(pc).Name(), x)
}
// 切片传入的实际也是指针
func testSlice(x []int) {
pc, file, line, _ := getRunTimeInfo()
fmt.Printf("[%s][line_%d][%s]: %p\n", file, line, runtime.FuncForPC(pc).Name(), x) //切片拿到的
}
func main() {
a := [2]int{1, 2}
fmt.Printf("raw pointer: %p\n", &a)
test(a)
testPtr(&a)
testSlice(a[:])
}
打印效果: 1
2
3
4raw pointer: 0x112e998e60b0
[d:/Documents/Desktop/alpha_go/main.go][line_14][main.test]: 0x112e998e60e0
[d:/Documents/Desktop/alpha_go/main.go][line_19][main.testPtr]: 0x112e998e60b0
[d:/Documents/Desktop/alpha_go/main.go][line_24][main.testSlice]: 0x112e998e60b0
区分指针数组和数组指针
数组指针:
*[N]T,星号在括号前
指针数组:
[N]*T,星号在括号后
数组指针: 1
2
3
4
5
6a := [3]int{1, 2, 3}
var p *[3]int = &a
a[2] = 5
fmt.Println((*p)[2]) //5
p[2] = 6
指针数组: 1
2
3
4
5
6
7
8
9a, b, c := 1, 2, 3
var arr [3]*int = [...]*int{&a, &b, &c}
arr1 := [...]*int{&a, &b}
*arr[0] = 3
*arr[2] = 5
*arr1[1] = 4
println(a, b, c) //3 4 5
数组array的长度是固定的,因此增删、排序对其操作实际很不方便,常用的做法是转换到切片的视角:
1
2
3
4a := [5]int{1,2,3,4,5}
s := a[:] //s和a指向同一个地址
s.append(s,6) //方便调用切片的各种算法和接口1
2
3
4
5
6
7
8
9
10
11
12
13a := [...]int{1, 2, 3, 4, 5}
s := a[:]
s = append(s, 7)
//产生动态扩容后,s和a不再指向同一块地址
s[3] = 10
//虽然扩容到6,但是仍然可以指定更长的空间,因为copy是值传递
var b [10]int
copy(b[:], s)
fmt.Printf("%#v ", a) //[5]int{1, 2, 3, 4, 5}, 可见s修改已经不再影响a了
fmt.Printf("%#v ", b) //[10]int{1, 2, 3, 10, 5, 7, 0, 0, 0, 0}
切片Slice
golang没有引用,引入切片方便了动态存储和扩容,特点如下:
切片是对底层数组的一个视图,本质是一种描述结构:
1
2
3
4
5type slice struct{
array unsafe.Pointer //底层数组指针
len int //长度,切片起始至末尾
cap int //容量,切片起始至底层数组末尾
}一个底层数组可以有多个切片视图,从数组到切片是指针传递,类似引用;而从切片到切片相当于结构体拷贝,为值传递;
长度可变,属于一种可变数组,空切片的len和cap都是0;
切片的初始化
需要区分nil切片和空切片,空切片含有合法的切片头,但len和cap都是0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//nil切片
var s1 []int
fmt.Println(s1 == nil, len(s1), cap(s1)) //true 0 0
//空切片
s2 := []int{}
fmt.Println(s2 == nil, len(s2), cap(s2)) //false 0 0
//这也是空切片,分配0个长度,1个容量
var s3 []int = make([]int, 0, 1)
fmt.Println(s3 == nil, len(s3), cap(s3)) //false 0 1
//len=3, cap=4
var s4 []int = make([]int, 3, 4)
fmt.Println(s4 == nil, len(s4), cap(s4)) //false 3 4
区分cap和len
主要是从数组切片而来,cap和len可能是不一样的,起点都是切片的起点,但cap的计数终点是数组的末尾,而len是切片的末尾,如:
1
2
3
4var a = [...]int{0, 1, 2, 3, 4, 5, 6}
s := a[2:4]
fmt.Println(len(s)) //2,仅包含下标为2、3元素
fmt.Println(cap(s)) //5,结尾为6,2-6共5个元素len==cap时,会重新分配内存,对于小切片,会按2倍策略增长;对大切片而言会采取平缓的增长策略,不同编译器版本有细节差异;
区分golang内存分配make和new
golang中也提供new操作,用于开辟新的内存,这个内存可能在栈上,也可能逃逸在堆上,但无需用户手动delete,栈上的数据会在生命周期结束被系统快速回收,在堆上的数据会被go的gc自动管理和回收;
对slice、array、map以及channel类型,golang提供了make初始化方法,对比new方法,除了分配内存,还会进行空间初始化操作,所以一般在需要基本数据类型指针的场合,使用new,而对于这四种类型,使用make,如果强行使用new,很可能遇到panic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package main
import (
"fmt"
)
func main() {
//int
v := new(int)
*v = 81
fmt.Println(*v) //ok,81
//array
arr := new([5]int)
(*arr)[0] = 81
fmt.Println(arr) //ok, &[81 0 0 0 0]
//slice
sli := new([]int)
(*sli)[0] = 81
fmt.Println(sli) //error: panic!
//map
mp := new(map[int]int)
(*mp)[0] = 81
fmt.Println(mp) //error: panic!
}
这是因为slice、map、channel都算类似引用的类型,golang给引用类型赋nil,直接给nil赋值会导致panic,因此必须使用make,除了开辟内存,还会将其初始化为0值而非nil:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package main
import (
"fmt"
)
func main() {
//int
v := new(int)
*v = 81
fmt.Println(*v) //ok,81
//array
arr := make([]int, 5)
arr[0] = 81
fmt.Println(arr) //ok, [81 0 0 0 0]
//slice
sli := make([]int, 5, 5)
sli[0] = 81
fmt.Println(sli) //ok,[81 0 0 0 0]
//map
mp := make(map[int]int)
mp[0] = 81
fmt.Println(mp) //ok:map[0:81]
}
参考链接:

