在 go
文件中头部的 package ***
下面使用 import "fmt"
这种语法导入. 类似于java. 例如:
package main
import "fmt"
import "math"
同时 go
还支持多导入的方式. 推荐使用这种方式导包. 例如:
package main
import (
"fmt"
"math"
)
注意1: 导入包之后, 使用导入包的方法时, 一般都是大写字母开头, 在 Go
中大写字母开头的方法类似于有 public
权限, 可以在外部调用, 而小写字母开头的方法, 则只能在 文件内部调用 或者 相同目录下同个包名的文件 间调用. 文件内部调用很好理解, 那什么是 “相同目录下同个包名的文件”? 其实就是指: 在相同目录下面, 如果有多个 Go
文件, 但是文件头部定义的 package ***
是一致的文件.
注意2: 在 Go
中相同目录下不允许出现拥有不同包名的文件. 例如下面这种情况在相同路径下是不被允许的:
文件1
package main
文件2
package test
注意3: 不允许使用特殊符号定义包名及函数名.
注意4: 导入的包必须使用, 否则编译报错.
Go
中函数可以没有参数或者接收多个参数, 定义接收的参数时, 参数类型定义在变量之后. 函数的最后要定义返回值的类型. 如果函数接收的多个参数是相同类型时, 只需定义最后一个类型, 其他可省略.
例如:
package main
import (
"fmt"
)
func main() {
fmt.Println(add(1, 2))
}
func add(a , b int) int {
return a + b
}
函数支持 多值返回 , 例如:
func Get(a, b , c int) (int , int){
return a + b , c
}
调用时, 返回的值就是两个 int
值
函数支持 命名返回值, 例如:
func GetResult(a, b, c int) (x, y int){
x = a + b
y = c
return
}
调用时相当于返回了 x
和 y
两个变量.
注意1: 不允许使用特殊符号定义函数名. 函数名使用驼峰命名法(即 aaBbCc
这种方式), 首字母大写代表 public
权限, 不同目录下通过导包可以调用, 首字母小写代表 private
权限, 只有该文件或同目录下相同包名的 go
文件可以调用.
注意2: 不允许定义多个相同函数名的函数, 哪怕接收的参数不同 (类似于不允许使用 java
中的方法重载) . 相同目录下的其他 go
文件也适用这个规则, a.go 和 b.go 中都含有 add() 函数也会造成编译失败. 可以理解为同个目录下的所有 go 文件到最后都会整合编译为一个统一的文件. 此外, 函数名区分大小写, 例如下面这种情况是被允许.
// 在相同目录下
// 文件 a.go 中有下列函数
func add(a , b int) int {
return a + b
}
func aDd(a , b int) int {
return a + b
}
// 文件 b.go 中有下列函数
func aDD(a , b int) int {
return a + b
}
注意3: 在不同目录间调用函数, 例如在 src
目录下定义有 hello 与 test 两个目录, hello 目录下的 hello.go 要去调用 test 目录下的 a.go 文件中的 Add() 方法, a.go 文件的包名是 package aaa , 如下所示.
// test 目录下的 a.go
package aaa
func Add(a , b int) int {
return a + b
}
那么需要做以下操作.
import "test"
然后在方法中调用 aaa.Add(1, 2)
package main
import (
"fmt"
"test"
)
func main() {
fmt.Println(aaa.Add(1, 2))
}
此外, 函数也可以定义为函数类型. 例如:
// 创建函数类型
type MyFunc func(a, b int) int
// 创建函数的具体实现, 在同一文件中, 只要函数参数与返回值类型相同即可视为函数实现.
func myFunc(a, b int) int{
return a + b
}
func main() {
// 创建函数变量, 等价于 var impl func(int, int) int
var impl MyFunc = myFunc
fmt.Println(impl(1, 2)) // 值为3
}
上面的定义方式也可以简写为下面这种 匿名函数
// 这里 impl 代表了函数变量, 之后可以使用 impl(1, 2) 这种方式来调用.
var impl = func(a, b int) int {
return a + b
}
进一步的简化甚至可以直接定义匿名函数并返回值
// c 的值为 3
var c = func(a, b int) int {
return a + b
}(1, 2)
使用 var
语句可以定义一个或多个变量. 并且可以像 java
的 成员变量, 局部变量 概念一样在函数内外定义, 例如:
package main
import (
"fmt"
)
var g, h, i bool
func main() {
var i bool = true
fmt.Println(g, h, i)
}
// 打印结果 false, false, true
调用时遵循就近原则, 方法中会优先使用方法内部定义的变量, 找不到的情况下会去方法外找.
初始化变量. 定义多个变量的情况下可根据需要一一对应赋值, 如果初始化是使用表达式,可以省略类型, 变量会从初始值中获得类型。在方法内部, 表达式也可以简写为 k := 1
这种形式, 不支持方法外如此简写.例如:
package main
import (
"fmt"
)
// 第一种定义方式
var g, h, i bool = true, true, false
// 第二种定义方式
var (
a int = 1
b bool = true
c string = "hello"
)
func main() {
// 第三种定义方式
var i, j = true, 1
k := 2
fmt.Println(i, j, k, a, b, c)
}
// 打印结果为 true, 1, 2, 1, true, hello
默认值, 当定义变量又没有为变量赋值时, 变量会拥有一个默认值.
数值类型为 0
,
布尔类型为 false
,
字符串为 ""
有符号(正号, 负号)整型
具体类型 | 取值范围 |
---|---|
int8 | -128 到 127 |
int16 | -32768 到 32767 |
int32 | -2147483648 到 2147483647 |
int64 | -9223372036854775808 到 9223372036854775807 |
无符号(无符号)整型
具体类型 | 取值范围 |
---|---|
uint8 | 0 到 255 |
uint16 | 0 到 65535 |
uint32 | 0 到 4294967295 |
uint64 | 0 到 18446744073709551615 |
特殊的 int
和 uint
参考: http://blog.cyeam.com/json/2017/12/18/go-int-size
在 32 位系统中, int
uint
类型占 4 个字节, 32位长度.
在 64 位系统中, int
uint
类型占 8 个字节, 64位长度.
根据该特性使用代码 const host32bit = ^uint(0)>>32 == 0
可判断系统是都为 32 位
类型别名
1) Unicode字符 rune
类型等价 int32
2) byte
等价 uint8
特殊类型 uintptr
官方解释为: An unsigned integer capable of storing a pointer value (advanced)
(意为一个足够储存指针值的无符号整型)
由系统决定占用位大小,足够存放指针,和C库或者系统接口交互
浮点数类型 float32
以及 float64
参考: https://www.cnblogs.com/orlion/p/6538706.html
正常的表现形式为 a float32 = 0.1
但也可以这样表示 a float32 = .1
另外一种表示方法是在其中加入指数部分。指数部分由”E”或”e”以及带正负号的10进制整数表示。例如:
3.9E-2 表示浮点数 0.039
3.9E-3 表示浮点数 0.0039
3.9E+1 表示浮点数39
3.9E+2 表示浮点数390
float32
和 float64
的区别类似 java
中 float
与 double
的区别, 主要在于长度的问题, float32
占了 4 个字节, float64
占了 8 个字节.
复数类型
复数类型接收一个实部 和 一个虚部 为参数并返回一个 复数类型。
复数类型有两个: complex64
和 complex128
。
实部和虚部应该为同一类型(float32 或 float64)
如果实部和虚部都是 float32
,该函数返回一个类型为 complex64
的复数。如果实部和虚部都是 float64
,该函数返回一个类型为 complex128
的复数。
复数类型的值一般由浮点数表示的实数部分、加号”+”、浮点数表示的虚数部分以及小写字母”i”组成,比如
c := 3.9E+1 + 9.99E-2i
复数也可以通过简短声明语法来创建:
c := 6 + 7i
类型转换
在类型之间的转换可以使用下列方式:
var i int = 1
var f float64 = float64(i)
var u uint = uint(f)
也可以简写为:
i := 1
f := float64(i)
u := uint(f)
类型推导
前文说过变量定义时可以简写为下面这种格式:
i := 1;
这种写法就是类型推导, 变量的类型会根据右边的值推导出来.
默认类型: 当使用简写时, 类型推导会有下面的默认类型出来.
使用 fmt.Printf("%T\n", i)
打印出来的类型分别是
i := 42 // int
f := 0.1 // float64
u := 1 + 2i // complex128
常量
常量使用 const 关键字定义
const i = 1;
常量可以是任何基本类型的值, 但不可使用简写 :=
语法.
数组类型
数组只能存放相同类型的数据, 定义方式如下.
var numbers = [3]int{1,2,3}
// 也可简写为
numbers := [...]int{1,2,3,4,5}
// 可以为数组定义类型别名. 如下
type Numbers [3]int
//此时可以使用 Numbers 代替 [3]int
var numbers = Numbers{1,2,3}
注意: 数据在定义时, 长度是固定的, 不可再改变长度.
访问各个位置的元素可以使用 numbers[0]
这种方式访问.例如修改元素值的方式如下:
numbers[0] = -1
获取数组长度的方式为:
length := len(numbers)
切片类型 slice
切片与数组一样可以保存若干个相同类型的数据, 但切片与数组的区别就是可以改变长度.每个切片值都会将数组作为其底层数据结构。
可以使用定义数组的方法定义切片. 例如:
slice := []int{1,2,3}
type Slice []int
slice := Slice{1,2,3,4}
还有一种操作数组的方式叫做“切片”,实施切片操作的方式就是 切片表达式 。例如:
// 定义数组
var number = [5]int{1,2,3,4,5}
// 切片操作
slice1 := number[1:4]
// 对切片再次切片操作
slice2 := slice1[1:3]
注意: 数组的容量总是等于其长度,而切片值的容量往往与其长度不同. 如下:
number := [5]int{1,2,3,4,5}
length := len(number)
slice1 := number[1:4]
count := cap(slice)
fmt.Println(slice," length = ", length, " count = ", count)
// 此时打印的结果为
[2 3 4] length = 5 count = 4
切片长度的计算方式是从 首个 索引 算到原始数组的 最后一个索引 的长度.如下图所示:
经过测试, 虽然此时 slice1
的长度为 4 , 但如果访问它最后一个元素时会在运行时报错.
如果想往后继续访问最后一个元素的数据, 可以如下操作:
slice1 = slice1[:cap(slice1)]
// 这样处理之后 slice 的结果为 2,3,4,5
如果想要在切片后限制更多元素的访问, 可以如此操作:
slice1 := number[1:4:4]
// 使用这种切片的方式后, slice1 切片再使用上面的方式也无法访问到后一个元素的数据了.
slice1 = slice1[:cap(slice1)]
// 此时切片获取到的数据是 2,3,4
如果想对切片进行拓展, 可以使用下面的操作:
// 增加元素
slice1 = append(slice1, 6, 7)
注意: 一旦扩展操作超出了被操作的切片值的容量,那么该切片的底层数组就会被替换
还有一种操作切片的方式是 复制 , 使用内置方法, 传入两个切片. 如下:
slice1 := []int{1,2,3}
slice2 := []int{0,0,0,0,0}
// 复制操作会用第二个参数的切片覆盖第一个参数的切片中相同索引的值.
copy(slice2, slice1)
// 此时打印第一个参数 slice2 的结果为 1,2,3,0,0
// 如果此时把参数替换过来如下.
copy(slice1, slice2)
// 则第一个参数 slice1 结果为 0,0,0
因此得出结论, 复制操作后, 第一个参数的切片长度不会被第二个参数所影响, 第二个参数只会把相同索引的值覆盖到第一个切片的相同索引位置.
字典类型 Map
定义方式如下, map
后的 [int]
是 key
, string
是 value
, 大括号中以 :
分隔键值对的形式定义内容.
m := map[int]string{1:"a",2:"b",3:"c"}
访问内容
m1 := m[1] // 值为 a
m[2] = m1 + "b"; // 值为 ab
// 添加字典元素
m[4] = "d"
// 经过测试此时如果打印字典 m , 取出的值是随机的, 不遵循先进先出的概念
// 如果此时获取不存在的值时, 不会出现异常, 可以使用如下判断
m[5] = ""
m5 , ok1 := m[5]
m6 , ok2 := m[6]
// 返回有2个值, 前面是返回的结果, 后面的 ok 类型为 bool, 代表是否有值
// 结果是 ok1 为 true, 即 m[5] 有值, 但值是空字符串. ok2 为 false, 即 m[6] 没有值
// 删除字典元素, 第二个参数代表键值
delete(m, 1)
通道类型
通道(Channel)是一种独特的数据结构。它可用于在不同 Goroutine
之间传递类型化的数据。并且是并发安全的。相比之下,之前几种数据类型都不是并发安全的。
Goroutine
翻译过来是 go程序
的意思, 可以看作是承载可被并发执行的代码块的载体。它们由Go语言的运行时系统调度,并依托操作系统线程(又称内核线程)来并发地执行其中的代码块。
类似于 java 中的 List
容器, 数据是先进先出的, 而且必须在定义时确定该容器的储存数据类型.
定义方式如下:
// 只能通过 make 方法定义, chan 代表通道类型, string 代表通道中储存数据的类型. 5 代表长度
ch1 := make(chan string, 5)
另外, make
方法也可以用来定义字典以及切片类型. 如下:
map1 := make(map[int] string, 5)
slice1 := make([]int, 5)
通道类型的储存操作如下:
// 储存字符串到通道
ch1 <- "value1"
// 取出字符串并赋值给变量, 遵循先进先出原则, 打印结果为 value1
value := <- ch1
// 判断是否有值应该如下接收, 注意读取后如果再执行一次读取会在运行时报错.
ch1 <- "value2"
value2, ok2 := <- ch1
// 关闭通道
close(ch1)
// 关闭通道后仍可从通道中读出数据, 但不可再写入数据, (另外通道不可重复关闭,会导致运行时异常)
// 此时如果使用 ch1 <- "value3" 会在运行时报错. 但仍可读取.
// 如果储存的数量已经到达通道的上限, 那么再往通道发送数据时会产生运行时异常.例如:
ch2 := make(chan string, 2)
ch2 <- "value1"
ch2 <- "value2"
ch2 <- "value3" // 在这一行会报错.
通道的缓冲和非缓冲之分
上面定义的有长度的通道成为 缓冲通道 , 而 非缓冲通道 则不能定义长度, 因为它只有一个长度,当非缓冲通道中存入数据后它就会被阻塞, 只有在值被取出后才可以继续工作.
非缓冲的通道值的初始化方法如下:
ch3 := make(chan string, 0)
在默认情况下, 通道都是 双向通信 的, 但也可以定义 单向通道, 即单独 只可接收 或 只可发送 的通道. 如下:
// 正常初始化不能指定为单向通道。但是在编写类型声明的时候可以指定:
// 声明一个 只可从里面接收数据出来 的接收通道, 注意 <- 这个接收操作符在 chan 左边, 意为数据导向 chan 外部
type Receiver <- chan int
// 声明一个 只可往里面发送数据 的发送通道, 接收操作符在 chan 右边意为数据导向 chan 内部
type Sender chan <- int
可以把一个双向通道转换为上述类型的单向通道, 但反过来却无法把单向通道转换为双向通道 或者 另一种单向通道, 而这正是单向通道的意义所在. 即在实际业务中我们通过使用单向通道的方式来约束数据源的可读可写权限.例如:
// 定义双向通道并发送数据
myChannel := make(chan int, 3)
myChannel <- 1
// 转换为发送通道并发送数据
var sender Sender = myChannel
sender <- 2
sender <- 3
// 转换为接收通道并接收数据
var receiver Receiver = myChannel
value1, ok1 := <- receiver
value2, ok2 := <- receiver
value3, ok3 := <- receiver
fmt.Println(value1, ok1) // 值为1 true
fmt.Println(value2, ok2) // 值为2 true
fmt.Println(value3, ok3) // 值为3 true
举例, 实际应用中, 我们可以在调用方法的时候给予它一个 发送通道 作为参数,以此来约束方法内部只能向该通道发送数据, 而方法处理完返回这个通道结果时我们可以把通道转换为 接收通道, 限制方法外部只可从通道中读取数据.