适合有其它编程语言经验(最好是C)的同学,快速上手go语言的相关特性,了解go语言的运行细节,并结合许多实用的go伪代码来服务于真实场景。
1.风格不统一
2.对硬件的调度,运行速度不够
3.处理大并发不够好
静态编译语言的安全和性能+动态语言开发维护的高效率
(1)语法层面支持并发,实现简单
(2)goroutine,轻量级线程,可实现大并发处理,高效利用多核
(3)线程间管道通信机制
(4)函数返回多个值
(5)CPS(communicating sequential process)并发模型
(6)切片slice,延时执行defer
参数:-w 输出重定向至文件
func main(){
} // 正确
func main()
{
} // 错误
(1)英文大小写,_,0-9
(2)不能数字开头
(3)严格区分大小写
(4)不包含空格
(5)_,空标识符,仅作为占位符,本身的值会被忽略
(6)不能系统关键字
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出**(共有)**的。例如,Pizza
就是个已导出名,Pi
也同样,它导出自 math
包。若首字母小写(私有)只能本包中使用。
pizza
和 pi
并未以大写字母开头,所以它们是未导出的。
在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。
执行代码,观察错误输出。
然后将 math.pi
改名为 math.Pi
再试着执行一次。
package main
import (
"fmt"
"math"
) //注意是()
func main() {
fmt.Println(math.Pi)
}
不同数据类型任何情况不能自动转换(低精度到高精度等),必须使用强制转换(显式转换)。
var int32_i int32 = 100
var int64_i int64 = int64(i) //变量i转换为int
1.转换后int32_i还是int32,只是把转换后的值赋予int64_i。
2.高精度->低精度(int64->byte),会溢出处理。
int32 + 10 为int32不能赋值到int64
var n1 int64 = 100
var n2 int32 = 100
n1 = n2 + 10 //n2 + 10为int32不能直接赋值int64,编译不通过
n1 = int64(n2) + 10 //编译通过
var n1 int32 = 12
var n2 int8 = 0
n2 = (int8)n1 + 128 //编译不通过,128不是int8
n2 = (int8)n1 + 127 //编译通过,但是溢出处理
(1)fmt.Sprintf
str = fmt.Sprintf("%d" , num_int)
(2)strconv
#strconv.Itoa int快速转为str
#FormatInt
strconv.FormatInt(int64(original_int64_varialble), radix)
#第一个输入必须int64,第二个为转换的进制2-36
#FormatFloat
#FormatBool
#strconv.ParseBool
b , _ = strconv.ParseBool(str)
#多返回值:bool与err,不需要的_忽略接收
#strconv.ParseInt
默认返回值数据类型为int64,需要其他int类型需要强制转换。
#若string为“hello",强制转换成int,则转换失败默认赋值为默认值0。
#变量直接储存值,内存通常分配在栈。(编译器逃逸分析可能分配在堆)
#变量储存地址,这个地址对应的空间才是存储的值,内存通常在堆上分配,当没有变量引用这个地址时,变成垃圾被GC回收。
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
数值(整型、浮点型)类型为 0
,
布尔类型为 false
,
字符串为 ""
(空字符串)。
1.var n1, n2, n3 int
2.var n, name, n3 = 1 , “tom”, 2
3.n, name, n3 := 1 , “tom”, 2 //类型推导
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
函数外的每个语句都必须以关键字开始(var
, func
等等),因此 :=
结构不能在函数外使用
//全局变量
var{
n1 = 1
name = "tom"
n2 = 3
}
10 / 4 = 2 //运算的两个数都是整型,因此转换为整形,数据溢出,结果为2
var i float = 10 / 4 //还是2
var i float = 10.0 / 4 //2.5,需要浮点数参与运算
a := b++ //错误,只能单独b++使用
if i++ > 0 { //错误
强制语法统一,独立使用i++,减少歧义
无–i,++i
从右向左运算
=, +=, -=, *=, /=, %=
<<=, >>=, &=, ^=, |=,
<<= // c = c << 2 , 右移两位后赋值c
&= // c = c & 2 , 与2位与后赋值c
**区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。**移动指针需要使用unsafe包
变量名不等于数组开始指针,如果p2array为指向数组的指针, *p2array不等于p2array[0]。
数组做参数时, 需要被检查长度。
1.只接受多值返回中的部分值
// 使用_进行占位
a, _, c = fun_return_abc()
2.两种返回方式
//
func swap(x, y string) (string, string) {
return y, x
}
//
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
if a > 0 {
fmt.Println("a")
}
if ... {
...
} else if{
...
} else {
...
}
同 for
一样, if
语句可以在条件表达式前执行一个简单的语句。
if
逻辑块之内。if 条件1 { // 1为真
...
} else if 条件2 { // 2为真
...
} else if 条件3 {
...
} else {
...
}
//执行完1后跳出条件判断
package main
// 推荐的引入包的方式
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else { //必须else不换行,保证这种格式
fmt.Printf("%g >= %g\n", v, lim)
}
// 这里开始就不能使用 v 了
return lim
}
func main() {
// 由内层开始执行,先pow最后println
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
1.循环变量初始化 i++
2.执行循环条件 i <= 10
3.判断条件(返回bool值);若为真,执行循环操作(语句块1)
4.执行循环变量迭代
5.反复1234,知道循环条件False
for i := 1 ; i <= 10 ;i++ {
语句块1
}
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
for {
if 循环条件表达式{ //决定dowhile还是while
break
}
循环操作
循环变量迭代
}
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
for i < 10 {
i++
}
for i := 0; i < len(str); i++ {
fmt.Println(str[i])
}
str = []rune(str)
for index, val := range str {
fmt.Println(val)
}
switch 表达式 {
case 表达式1, 表达式2, ...:
语句块1
case 表达式3, 表达式4, ...:
语句块2
default:
语句块
}
switch、case后可以跟常量、变量、函数的返回值
case后的数据类型必须和switch后的数据类型严格统一、完全一致
case后可以跟多个表达式
case 5, 10, n1 :
语句块
若case后为常量(只要是变量就可以),则不同的case后的常量之间不能数值重复
default语句块不是必备的
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
//在switch内声明变量,不推荐
func main() {
switch t := time.Now() {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
case a:
语句块1
fallthrough //会判断下面的case
case b:
语句块2
判断interface变量中实际指向的变量类型
label1: {
label2: {
label3: {
break label1 // 若没有标签默认跳出一层,也就是跳出label3这一层,跳出最近的一层for
// label1 等于break break break
}
}
}
通常与当前.go上一级文件目录名称保持一致,一般为小写字母
首字母大写(可以是变量名、函数名)为可导出(公有、可导出、跨包使用),小写(私有、不可导出)
注意区分包名和包所在上级目录文件夹名称
//被调用文件,在/.../dir下
package utils //1.utils为包名,package规定打包的包名 2.package第一行
//调用文件
package use_utils
import (
"/.../dir" //在dir路径下寻找包
)
utils.TestFun() //用包名调用函数
import (
utils "/.../dir" //命名包别名为utils,后续引用**只能**使用别名,不能用原包名
)
同一包下不能有相同的函数名(以包做命名空间)
一个包也就是一个文件夹下可以有多个.go文件,但是都属于一个包,因此不能有相同的函数名和全局变量名
如果需要编译成一个可执行文件,则需要将这个包声明为main包,其它的库文件名称可以自定义
go build -o \bin\test.exe(保存路径) \...\main(需要编译的项目目录)
import (
"a"
_ "b" //引入不使用
)
func 函数名 (形参列表) (返回值列表) {
执行语句
return 返回值列表
}
函数中的变量是局部的,函数外不生效
基本数据类型和数组都是值传递,即值拷贝。在函数内修改,不会影响到原来的值。(不同的物理地址,可能变量名相同)
引用类型传递,传递的是被引用值的地址或者索引,因此修改的是同一个物理地址(指针),但是地址本身还是值传递
严格不允许函数名称重名(不支持函数重载)
在go中函数也是一种数据类型,可以赋值给一个变量,则该变量是一个函数类型变量,通过此变量可以调用函数
func myFun(funvar func(int, int) int, num1 int, num2 int) { //funvar是函数变量形参名字
return funvar(num1, num2)
}
type mySum func(int, int)int //mySum等价于func(int, int)int函数数据类型
//注意在虽然本质都是同一种类型,但是编译器认为两个是两种数据类型
//主要作用是返回时按照函数返回值列表顺序接收
func test(n1 int, n2 int) (n3 int, n4 int){
n3 = n1
n4 = n2
return //直接return,按照
}
n3, n4 = test(1,2)
//支持0到多个参数
func sum(arg... int) sum int { //arg为标识符,可以是其它名称
}
//支持1到多个参数
func sum(n1 int, arg... int) sum int {
}
//求一个到多个int的和
func sum(n1 int, vars_int... int) int {
sum := n1
for i :=0; i < len(vars_int); i++ {
sum += var[i]
}
return sum
}
栈区(通常存放基本数据类型)、堆区(通常存放引用数据类型)、代码区(存放代码)、全局区、常量区…(go编译器存在逃逸分析机制)
栈区函数结束释放,堆区当这个引用类型没有任何变量引用这个地址被GC回收
运行一次函数在栈区中拥有一个命名空间(下面的demo适用于栈区的基本基本数据类型)
func test (n1 int) { //使用栈区中test栈区中的n1来接收
n1 = n1 + 1
fmt.Println(n1) //输出11
}
func main {
n1 := 10 //在栈区中的main栈区创建n1
test(n1) //main栈区n1对test栈区n1赋值,实参对形参赋值
fmt.Println(n1) //调用完毕,回收test栈区,输出10
}
栈区 ------ test栈区
|
--- main栈区
|
--- ...
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=" , n)
}
//输出 2 2 3
func test(n int) {
if n > 2 {
n--
test(n)
}else{
fmt.Println("n=" , n)
}
}
//输出 2
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被go运行框架调用。
文件有全局变量定义:变量定义->init->main
init主要作用:完成初始化工作
引入的包有init时完整执行顺序:
package main //1
import ( //2
"/.../utils"
)
func init() { //6
}
func main() { //7
}
package utils //3
func init() { //4
}
func main() { //5
}
func main() {
res1 := func (n1 int, n2 int) int {
return n1 + n2
}(10, 20)
}
func main() {
func_name := func (n1 int, n2 int) int {
return n1 + n2
}
res1 := func_name*(10, 20)
}
// 累加器
func AddUpper() func (int) int {
// 闭包开始
var n int = 10
return func (x int) int { 返回的是一个匿名函数,但是函数引用了函数外的n,因此匿名函数和n形成了一个整体,构成了闭包
n = n + x
return n
}
// 闭包结束
}
func main() {
f := AddUpper()
fmt.Println(f(1)) // 10 + 1 = 11
fmt.Println(f(2)) // 11 + 2 = 13
fmt.Println(f(3)) // 13 + 3 = 16
}
// 返回的数据类型是fun(int)int
// 关键是返回的函数使用到了那些变量,函数和它使用的变量构成闭包
// 需求:1.判断文件名是否有.后缀,若有返回直接文件名(直接返回name.xxx),若无加上.xxx返回(name.xxx)
2..xxx可以修改即可
3.被调用的时候,大部分时间后缀使用.jpg
func MakeSuffix (suffix string) func (string) string {
return func (name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
} else {
return name
}
}
}
f := MakeSuffix(".jpg")
f("1") //返回1.jpg
f("2.jpg") //返回2.jpg
//闭包 = 匿名函数 + suffix
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
package main
import "fmt"
func main() {
//当执行到defer语句时,会语句将压入defer栈(独立栈)
//**当函数执行完毕后,按照先入后出的方式出栈执行**
defer fmt.Println("!") //3
defer fmt.Println("world") //2
fmt.Println("hello,") //1
}
func main() {
n1 := 1
n2 := 2
// 在defer语句入栈时,会将相关的**值拷贝**(和原变量为两个独立的变量)同时入栈
defer fmt.Println(n1) //1
defer fmt.Println(n2) //2
n1++
n2++
fmt.Println(n1+n2) //5
}
func test() {
file = openfile()
defer file.close() //不用考虑具体啥时候关闭文件句柄
...
}
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
值类型默认使用值传递,引用类型默认使用引用传递
如果希望函数外修改函数内变量,只能引用传递,利用&以指针的方式操作变量
地址1 地址2
| -> |
值1 值2
拷贝效率低
值类型:基本数据类型int、float、bool、string,数组,结构体
地址1 -> 地址2
| |
值1 值1
拷贝效率高
引用类型:指针、切片、map、管道chan、interface等
buildin,内建函数不用import直接使用
len(str),按字节统计字符串长度或数组长度(go使用utf8,ascii字符(字母和数字)占一个字节,一个汉字(或者其它语言)占用三个字节)
中文字符串遍历,r := []rune(str)
字符串转整形,n, err := strconv.Atoi(""),可以利用err判断输入是否是纯数字;整数转字符串,str = strconv.Itoa(),不存在不能转换
字符串转byte,转为ascii,var bytes = []byte(“str”);byte转字符串(ascii),str = string([]byte{97, 98, 99})
10进制转2、8、16进制,str = strconv.FormatInt(123, 2)
查找子串是否在指定字符串,strings.Contains(“abcddd”, “abc”) // true
统计字符串由几个指定的子串,strings.Count(“abcddd”, “ddd”) // 3
不区分大小写比对字符串,strings.EqualFold(“str”, “str”);不区分大小写,str == str
返回子串第一次出现的index,若无返回-1,strings.Index(“adcddd”, “ddd”) // 3;返回子串最后一次出现的index,若无返回-1,strings.LastIndex(“adcddd”, “ddd”)
使用子串替换另一个子串,n为第n个,n=-1表示全部替换,strings.Replace(“abcddd”, “ddd”, “eee”, n),可以是变量传入
按照指定字符拆分字符串,strings.Split(“1,2,3,4”, “,”),返回一个字符串数组
大小写转换,全部变为大写或小写,strings.ToLower(str),strings.ToUpper(str)
去掉字符串两边的所有空格,strings.TrimSpace(" test ")
将字符串左右两边指定的字符去掉,不考虑去除去字符串的顺序,strings.Trim("!ahelloa!", “!”) // hello
将字符串左(右)侧指定字符去掉,不考虑去除去字符串的顺序,strings.TrimLeft("!hello!", “!”)、strings.TrimRight("!hello!", “!”)
是否以指定的字符串开头、结尾,strings.HasPrefix(“abc”, “a”)、strings.HasSuffix(“abc”, “c”)
go追求简洁优雅,不支持try…catch.finally
go引入的异常处理方式:defer、panic、recover
go抛出一个panic异常 -> 在defer中的recover中捕获这个异常 -> 处理异常
func test() {
defer func () {
err := recover() // 捕获异常,将异常返回err变量(error类型)保存
... // 处理异常
}() // 匿名函数,无输入,无输出
... // 此处抛出错误
... // 跳过错误,抛出异常后继续执行
}
func test() {
defer func () {
if err := recover() ; err != nil { // 捕获 + 判断
...
} // 捕获异常,将异常返回err变量(error类型)保存
... // 处理异常
}() // 匿名函数,无输入,无输出
... // 此处抛出错误
... // 跳过错误,抛出异常后继续执行
}
// 读取配置文件,目录下有无指定文件名文件
func readConf(name string) (err error) {
if name == "config.ini" {
return nil
} else {
return errors.New("file name error!")
}
}
func test() {
err := readConf("...")
if err != nil {
panic(err)
}
...
}
在go中数组时值类型,arrayName为变量名(a),数组为变量数据(1)。(var a int = 1)
创建数组后自动为每个数组元素赋零值(详情见零值章节)
arrayName地址为数组首地址(=第一个元素的地址),arrayName是值类型,直接存放数组(数组看做一个和int等一样的值);数组地址为:首地址(变量arrayName地址、第一个元素地址)+ n * 数组类型长度(例如:int64,为8个字节,地址加8)
数组时多个相同数据类型的数据的组合,数组一旦声明,其长度就固定,不能动态变化。(数组长度也是数组信息的一部分)
注意切片和数组的区别,var arr []int 这种写法arr是切片
数组可以是基本数据类型也可以是引用类型,但是只能全部元素是一种数据类型
数组下标从0开始
go数组属于值类型,在默认情况下进行值传递,因此进行值拷贝不会影响原数组
想函数外修改函数,需要传递指针,使用引用传递
var array [3]int = [3]int{1, 2, 3}
var array = [3]int{1, 2, 3}
var array = [...]int{1, 2, 3} // ...系统自动判断大小
var array = [...]int{1: 2, 0: 1, 2: 3} // 数组下标+数据
array := [...]int{1, 2, 3} // 类型推导,最简洁
for i := 0; i < len(array); i++ {
...
}
for index, value := range array {
...
}
// 1. 第一个返回值index是数组下标
// 2. 第二个value是在该下标位置的值
// 3. index、value都是在for内部可见的局部变量
// 4. 若遍历数组时不需要index或value,使用_替代
// 5. index和value是两个变量接受返回值,名称不固定,习惯使用index和value
var arr [n1][n2]int //n1,n2为数组真实大小,不是从0开始
arr[n3][n4] = i //n3,n4为索引从0开始计数
&arr[0][0] //第一个一维数组首地址
&arr[1][0] //第二个一维数组首地址
var arr [n][n]int = [...][n]int{{...},{...}} // 只有一维数组也就是第一个位置可以...,因为是按照初始化时的个数来自动确定个数
var arr = [n][n]int{{...},{...}}
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ { // 注意len(arr[i])
...
}
}
for i1, v1 := range arr {
for i2, v2 := range v1 { // 注意range v1
}
}
切片是数组的引用(修改slice,原数组也会改变),切片的长度可以变化,是一个动态的数组。(底层追加的方式是1.有容量的概念存在一定的冗余;若冗余不足,2.拷贝原切片指向的数组的值到一个新的空间,然后创建切片返回新的切片,然后通过切片变量接收)
数组a[1](有确定大小),切片a[](无确定大小)
与数组相比,遍历、访问元素(silce[n])、len()用法相同
定义:var sliceName []int
切片有容量的概念(容量可以动态变化),也就是目前能存放最大元素的个数,使用cap()函数可以查看;一般容量是元素个数的两倍
切片本质是一个结构体:1.首个元素的首地址 2.长度 3.容量\
切片必须定义后,赋值后再使用
切片可以引用切片(或其它引用变量)
在初始化时不能越界,但是可以动态增长。可以使用append函数动态追加元素
append、copy的所有对象只能是切片,不能是数组
slice3 = append(silce3, 400, 500, 600) // append的方式是创建一个新的切片(数组),然后返回切片通过赋值传递给切片
slice3 = append(slice3, slice1...) // 切片追加切片
var array [3]int = [3]int{1, 2, 3}
silceName := array[0:2] // 引用array数组起始下标为0,终止下标为2**(不包含2)**
var slice_name []type = make([]type, len, cap(可以忽略))
var slice []int = make([]int, 4, 8)
slice[0] = 100
var silce_name []int = []int{1, 2, 3}
for i := 0; i < len(slice); i++ {
...
}
for i, v := range slice {
... //i为index,v为value
}
拷贝和被拷贝的两个数据空间是独立的
copy(slice1, slice2) // slice1内容拷贝到slice2,若容量1>2优先数组下标0开始,剩下的不拷贝;2>1从数组下标0开始拷贝,2多余剩下的内容不变
// 数组截取
str := "hello,world!"
str_slice := str[7:] // world!
// 错误
str[0] = 'z' // str本质是字符串结构体,无法通过数组的方式直接寻址,修改字符串
// 将string->[]byte/[]rune(byte或rune切片),然后修改
arr1 := []byte(str)
/*
1.如果处理中文需要转为[]rune
2.原因:go中string为byte数组(以字节为单位),go使用utf-8,ascii内的字符(英文、数字等)一个字节,其他语言(中文等)三个字节,因此要使用rune
*/
arr1[0] = 'z'
//
var mapName map[int]string
mapName = make(map[int]string, 10)
//
var mapName = make(map[int]string, 10)
// 底层自动make
var mapName map[int]string = map[int]string{1:"no1", 2:"no2", } // 注意,
mapName := map[int]string{
1:"no1",
2:"no2", // 注意,
}
// 一个学生信息包含:name、sex、address
//为最外层map分配空间
studentMap := make(map[int]map[string]string) // 不输入大小默认为1
// 为具体的学生分配空间
studentMap[1] = make(map[string]string, 3)
studentMap[1]["name"] = "..."
studentMap[1]["sex"] = "..."
studentMap[1]["address"] = "..."
map["key"] = value // 如果key还没有就增加,如果已经存在就更新(也就是修改)
delete(map, "key") // 如果key不存在,不会报错,也不会操作
// 如果全部删除所有的key,推荐2
// 1. 遍历key
// 2. map=make(),make一个新的同名map,让原来的成为垃圾被gc回收
val, res := map[“1”] // val储存返回键值,res返回是否存在键值,true存在,false不存在
for k, v := range map {
...
}
var mapSlice []map[int]string // 定义map切片
mapSlice = make([]map[int]string, 2) // 为slice分配内存
mapSlice[0] = make(map[int]string, 2) // 为map元素分配内存
// 在slice中追加map
newMap := map[int]string {
...
}
mapSlice = append(mapSlice, newMap)
key排序,key无序,每次输出可能顺序都不一样
排序思路,先将map的key放入切片,对切片排序,遍历切片,按照key输出map
var keys []int
for k, _ := range map {
keys = append(keys, k) //遍历插入key切片
}
sort.Ints(keys) // 使用自带方法排序
for _, k := range keys {
map[keys] //按照key的顺序遍历输出
}