Go 语言结构
初识Go语言
Go 语言结构
package main //①
import "fmt" //②
func main() { //③
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
代码说明:
① 定义包名,必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
② 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
③ 程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
说明:对比Java中的public 与 protected,Go语言中当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
注释
Go语言支持两种注释:
行注释以//开始,直到出现换行符时结束。
块注释以/* 开始,以*/结束
标识符
Go语言标识符是一个非空的字母或数字串,其中第一个字符必须是字母,该标识符不能是关键字的名字。
标识符是区分大小写的,以大写字母开头的标识符是公开的---以Go语言术语来讲就是可以导出的。其他的任何标识符都是私有的---以Go语言术语来讲就是未导出的。
行分隔符
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。有两个地方必须使用分号,即当我们在一行中放入一条或多条语句时,或者使用原始的for循环语句时。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。
Go语言基本数据类型
- 布尔型: bool,布尔型的值只可以是常量 true 或者 false。 默认值为false
Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
整型:int8,byte,int16,int,uint,uintptr等
浮点型:float32(精确到小数点后6位)、float64(精确到小数点后15位),默认为 float64
复数类型:complex64(两个float32组成),complex128(两个float64组成,这个为复数默认值)
字符串:string
字符类型:rune 等同于 int32
错误类型:error
派生类型:指针类型(Pointer),数组类型(array),结构化类型(struct),Channel 类型,函数类型,切片类型,接口类型(interface),Map 类型
数据类型的长度根据运行程序所在的操作系统类型所决定的。
比如 int类型 在32位或64位操作系统中,各自的长度就不一样。
类型 | 符号 | 长度范围 |
---|---|---|
uint8 | 无符号 | 8位整型 (0 到 255) |
uint16 | 无符号 | 16位整型 (0 到 65535) |
uint32 | 无符号 | 32位整型 (0 到 4294967295) |
uint64 | 无符号 | 64位整型 (0 到 18446744073709551615) |
int8 | 有符号 | 8位整型 (-128 到 127) |
int16 | 有符号 | 16位整型 (-32768 到 32767) |
int32 | 有符号 | 32位整型 (-2147483648 到 2147483647) |
int64 | 有符号 | 64位整型 (-9223372036854775808 到 9223372036854775807) |
**浮点类型与其他数字类型的长度 **
类型 | 长度 |
---|---|
float32 | math.MaxFloat32大约是3.4e38。最小的非负值大约是1.4e-45。 |
float64 | math.MaxFloat64大约是1.8e308。最小的非负值大约是4.9e-324。 |
byte | 类似 uint8,是它的别名,两个可以直接赋值,无需转换。区别自定义类型 |
rune | 类似 int32,是它的别名,两个可以直接赋值,无需转换。区别自定义类型 |
int | 与 uint 一样大小,默认的整数类型,依据平台,分为 32位和 64 位 |
uintptr | 无符号整型,用于存放一个指针 |
如果需要在不同的数值类型间进行数值运算或者比较操作,那么就必须进行类型转换,通常是将类型转换为最大的类型以防止精度丢失,类型转换采用 type(value)的方式进行。
若需要进行缩小尺寸的类型转换,我们就需要自定义向下转换函数
比如 int 与 uint8的转换
func IntToUint8(a int)(uint8,error) {
if 0<=a && a<= math.MaxUint8{
return uint8(a),nil
}
return 0,fmt.Errorf("%d is out of the uint8 range",a)
}
字符串
在go语言中,字符串是使用UTF-8格式编码的只读的Unicode字节序列。每个字符对应一个rune类型。一旦字符串变量赋值之后,内部的字符就不能修改,英文是一个字节,中文是三个字节。
使用range迭代字符串时,需要注意的是range迭代的是Unicode而不是字节。返回的两个值,第一个是被迭代的字符的UTF-8编码的第一个字节在字符串中的索引,第二个值的为对应的字符且类型为rune(实际就是表示unicode值的整形数据)。
默认值是空字符串,而非NULL
const s = "Go语言"
for i, r := range s {
fmt.Printf("%#U : %d\n", r, i)
}
-----output-----
U+0047 'G' : 0
U+006F 'o' : 1
U+8BED '语' : 2
U+8A00 '言' : 5
Go语言变量
在数学概念中,变量表示没有固定值且可改变的数,从计算机角度来讲,变量是一段或者多段用来存储数据的内存。
作为静态语言,go变量有固定的数据类型,变量类型决定了变量内存的长度和存储格式。
我们只能修改变量值,无法改变类型。
在编码阶段,我们用一名字来表示这段内存,但是编译后的机器码从不使用变量名,而是直接通过内存地址来访问目标数据。
- 指定变量类型:var + 变量名 + 类型,比如
var v1 int = 10
- 根据值自行判定变量类型:var + 变量名,比如
var v1 = 10
- 变量声明再赋值
var v1 int
v1 = 123
- 省略 var 关键字,注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误,而且这种不能声明全局变量,也就是说只能在函数内使用。比如
v1:= 10
:= 是用来明确表达同时进行变量声明与初始化的工作,类型自动推导。
:=
的使用
它只能被用在函数体内,而不可以用于全局变量的声明与赋值。
声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误。但是全局变量是允许声明且不使用。 同一类型的多个变量可以声明在同一行
在下面代码中使用 := 就会产生影子变量,一般情况下不要出现下面这种情况的代码
func main() {
var a,b,c = 1,2,3//①
for a:=4;a<5 ;a++ { //内部a覆盖了外部a的值
b:=5 //内部b覆盖了外部b的值
c =6 //依旧是外部的c
fmt.Printf("inner:a=%v, b=%v, c=%v\n",a,b,c)
}
fmt.Printf("outer:a=%v, b=%v, c=%v\n",a,b,c)
}
----output----
inner:a=4, b=5, c=6
outer:a=1, b=2, c=6
上面代码中其实for是创建了一个新的作用域,属于内部作用域,跟①不是在同一个作用域
:=
模式并不总是重新定义变量,也有可能是赋值操作
func main() {
x := 100
println(&x)
x,y := 200,"abc" //在这里,x为赋值操作,y才是变量定义
println(&x,x)
println(&y,y)
}
---output---
0xc00008cf38
0xc00008cf38 200
0xc00008cf40 abc
:=
变为 赋值操作的前提条件是:至少有一个新变量被定义,且必须是同一个作用域,否则会产生影子变量。
多变量声明
//类型相同多个变量, 非全局变量var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
//和python很像,不需要显示声明类型,自动推断
var vname1, vname2, vname3 = v1, v2, v3
//出现在:=左侧的变量不应该全是已经被声明过的,否则会导致编译错误
vname1, vname2, vname3 := v1, v2, v3
// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
多重赋值功能
将 i 与 j 的值互换
i,j = j,i;
多重返回值与空白标识符 _
比如GetName() 函数中会返回 三个值:name1,name2,name3,而我们只需要第三个值 name3,那么我们可以采用以下方式取得。前两个值使用 空白标识符 _ 抛弃不用。
//_ 表示丢弃这个值
_,_,name3 = GetName();
空白标识符 _ 在Go语言中是一个只写变量,只能写入,无法读取。它有两种用途:
-
比如在导包时,使用 _ 空标志符导入一个包时,就是想执行里面的init函数
import _ "github.com/go-sql-driver/mysql"
舍弃不需要的值
但是空白标记符在函数参数上使用时,传参并不能忽略该值
func main() {
test(1,2,"abc")
}
func test(x,y int,s string ,_ bool) {
fmt.Println(x,y,s)
}
会报如下错误
.\mian.go:6:6: not enough arguments in call to test
have (number, number, string)
want (int, int, string, bool)
变量零值
type user struct {
name string
}
var (
a int
b bool
str string
浮点 float32 //中文可以作为变量标识符
n *user
u user
var demo1 [10]int
var demo2 []int
var demo3 map[string]string
)
当一个变量被var声明之后,系统自动赋予它该类型的零值:
- int 零值为 0
- float 零值为 0.0
- bool 零值为 false
- string 零值为空字符串""
- 指针零值为 nil
- 结构体的零值为各字段的零值集合
- 数组的零值为 [0 0 0 0 0 0 0 0 0 0]
- 切片零值为 nil
- map零值为 nil
- 函数零值为nil
- channel零值为nil
- interface零值为nil
这些变量在 Go 中都是经过初始化的。大部分类型的零值在内存中占据的字节都是零,为什么不是所有呢?因为这依赖于编译器。
nil值
只有pointer, channel, func, interface, map, slice
这些类型的值才可以是nil
。结构体变量和数组变量并不可以为nil
,将一个变量声明为 nil时,需要指出该变量的类型,编译器无法猜出该变量的具体类型
package main
func main() {
var x = nil // 错误
_ = x
}
在一个 nil 的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic:
package main
func main() {
var a [10]int
a[0] = 1
fmt.Println(a) // 正常打印
var m map[string]int // 或者是 var x5 map[string]int = nil
m["one"] = 1
fmt.Println(m) // panic: assignment to entry in nil map
}
并不是所有的类型都可以声明为 nil
var x = nil //错误
var x interface{} = nil //正确
var x string = nil //错误,""是字符串的零值
var x error = nil //正确
var x map[string]int = nil //正确
var x user = nil //错误
var x [10]int = nil //错误
nil
值可以比较,比如声明一个切片或者字典
func main() {
var demo1 []int
var demo2 map[string]string
fmt.Println(demo1 == nil) //true
fmt.Println(demo2 == nil) //true
}
const关键字
常量是一个简单值的标识符,在程序运行时,不会被修改的量。被 const 修饰的常量不能再被重新赋予任何值。可以被 const 修饰的数据类型只能是:布尔型、数字型(整数型、浮点型和复数)和字符串型,字符。在const 定义中,对常量名没有强制要求全部大写,不过我们一般都会全部字母大写,以便阅读。
常见的定义格式
//显式类型定义:
const B string = "abc"
//隐式类型定义,编译器可以根据变量(常量)的值来推断其类型。
const B = "abc"
同时声明多个常量
const(
NUM1 = 1
NUM2 = 2
NUM3 = 3
)
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
没有被使用的常量并不会引发编译错误。
在Go语言中,预定义了 true
,false
,iota
常量。
iota关键字
自增默认类型为 int,初始值为0,可被编译器修改的常量,每次出现 const 就会重置为 0 ,在下一个 const出现之前,每出现一次 iota,值就会加1。
//示例1
func main() {
const (
c0 = iota;
c1 = iota;
c2 = iota;
)
fmt.Println(c0,c1,c2)// 0 1 2
}
上面的const 赋值语句相同,因此可以简写为下面实例,后面常量如果没有赋值,则继承上一个常量值。
//示例2
func main() {
const (
c0 = iota
c1
c2
)
fmt.Println(c0,c1,c2)// 0 1 2
}
iota 的起始行 为 const 的第一行,哪怕第一行并没出现 iota,且后续自增按行序递增,而不是按上一取值递增
//示例3
func main() {
fmt.Println(c0,c1,c2) // 1 1 3
}
const (
c0 = 1 // 在这行,iota 就已经被初始化为 0,并开始在下面每行 iota 值加1。
c1 = 1
c2 = 1 + iota
)
若有新的常量声明后,iota 不再向下赋值
//示例4
func main() {
fmt.Println(c0,c1,c2) // 0 8 8
}
const (
c0 = iota
c1 = 8
c2
)
自增类型默认为 int,可以显式指定类型,进行类型转换,要注意类型取值范围。
const (
a = iota
b float32 = iota
c = iota
)
常量值也可以是某些编译器能在编译期计算出结果的表达式
const (
a = len("hello world")
b float32 = unsafe.Sizeof(uintptr(0))
)
常量与变量的区别
const x = "java"
var y = "golang"
func main() {
println(&x,x)
println(&y,y)
}
---output---
./main.go:16:10: cannot take the address of x
不同于变量在运行期分配存储内存(非优化状态),常量通常会在预处理阶段直接展开,作为指令数据。
常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获得常量的地址。
常量声明方式不同,对编译器的影响
const x = 100 //隐式类型定义
const y byte = x //正常进行,相当于 const y byte = 100
const a int = 100 //显式指定常量类型,编译器会做强类型检查
const b byte = a //会报错:cannot use a (type int) as type byte in const initializer
枚举
由于Go语言并不支持Enum 关键字,但可以使用 const 与 iota 关键字来实现枚举
func main() {
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Firday
Saturday
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Firday, Saturday)
}
----output----
0 1 2 3 4 5 6
注:以大写字母开头的常量在包外可见,小写的不可见。
//存储单位的常量枚举
type ByteSize float64
const (
_ = iota // 通过赋值给空白标识符来忽略值
KB ByteSize = 1<<(10*iota)
MB
GB
TB
PB
EB
ZB
YB
)
浮点数的比较
import "math"
//p为自定义精度
func IsEqual(f1,f2,p float64) bool {
return math.Abs(f1 - f2)< p
}
复数表示
var v3 complex64
var v4 complex128
var v5 complex64
func main() {
v3 = 3.2 + 12i
v4 = 3.2 + 12i
v5 = complex(3.2, 12i)
对于一个复数 z = complex(x,y); 通过Go内置函数 real(z)获得该复数的实部x,通过imag(z)获得虚部y
类型转换
go强制要求使用显式类型转换
如果转换的目标是指针/单向通道/没有返回值的函数类型,那么必须将其用括号扩起来
func main() {
x := 100
p := (*int)(&x) //正确
p := *int(&x)//错误
}
自定义类型
可以使用 type 基于现有基础类型,结构体,函数类型创建用户自定义类型。
//将MyInt定义为int类型
type myInt int
通过Type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
和 var,const 类似,可以将多个 type 合并成组
func main() {
type ( //组
user struct { //自定义结构体
name string
age uint8
}
event func(string) bool //自定义函数类型
)
u := user{
name: "zhangsan",
age: 20,
}
var f event = func(s string) bool {
println(s)
return len(s) == 0
}
}
即便自定义类型的底层数据结构相同,也不能表示它们之间有什么关系,它们是属于完全不同的两种类型。
自定义类型不会继承底层数据结构的方法,不能看作是底层数据结构的别名,不能做隐式转换,不能直接用于比较表达式。
func main() {
type data int
var d data = 10
var x int = d //不能做隐式转换
var x int = int(d) //显式转换可以
println( x == d) //不能直接用于比较表达式
println( x == int(d)) //可以比较
}
自定义类型和类型别名区别
//定义一个别名
type myInt = int
//定义一个自定义类型
type myInt1 int
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型,拥有相同的底层结构。但是反过来讲拥有相同的底层结构不一定是别名。
之前见过的rune
和byte
就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
类型别名只会在代码中存在,在编译完成后。类型别名就不存在了
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
自增
自增,自减不再是运算符,只能是独立语句,不能用做表达式,也不能前置
func main() {
a := 1
++a //不能前置
--a //不能前置
a++
a--
if (a++) > 1 { //不能用作表达式
}
p := &a
*p++ //相当于(*p)++
}
指针
不能将内存地址与指针混为一谈。
内存地址是内存中每个字节单元的唯一编号,而指针是一个实体。
指针自己也需要分配内存空间,指针是一个专门用来保存地址的整形变量。
p:= &x | x := 100 | |
---|---|---|
memory | 0x1200 | 100 |
address | 0x800 | 0x1200 |
- 取址运算符 ‘&’ 用于获取对像地址
- 指针运算符 ‘*’ 用于间接引用目标对象
- 二级指针用于获取 指向指针的指针的存放地址
并不是所有的对象都能取地址,比如获取 map的元素地址
判断两个指针是否相等:要么指向同一快地址,要么都为nil。
指针支持相等运算,不支持加减运算和类型转换。
可以通过 unsafe.POinter
将指针转换为 uintptr
后进行加减运算
值传递还是指针传递
不管是指针,引用类型,还是其他类型参数,都是值拷贝,区别无非就是拷贝目标对象或拷贝指针而已。
func main() {
a := 100
p := &a
fmt.Printf("pointer: %p, target: %v\n",&p,p)
test(p)
}
func test(x *int) {
fmt.Printf("pointer: %p, target: %v\n",&x,x)
}
---output---
pointer: 0xc000006028, target: 0xc00000a0c8
pointer: 0xc000006038, target: 0xc00000a0c8
表面上看传递指针比较好,但是复制的指针会延长目标对象生命周期,还有可能会导致它被分配到堆上
要使用函数改变int,string 的值并且函数外的值也要受影响,可以使用二级指针或者返回值
func main() {
a := 100
p := &a
test(&p)
fmt.Println(*p)
}
func test(x **int) {
a := 200
*x = &a
}
---output---
200