推荐阅读:

基础语法


注意事项

  • Go源文件以 "go" 为扩展名
  • 与Java、C语言类似,Go应用程序的执行入口也是main()函数
  • Go语言严格区分大小写
  • Go不需要分号结尾
  • Go编译是一行一行执行,所以不能将类似两个 Print 函数写在一行
  • Go语言定义的变量或者import的包如果没有使用到,代码不能编译通过
  • Go的注释使用 // 或者 / /

关键字

if      for     func    case        struct      import
go      type    chan    defer       default     package
map     const   else    break       select      interface
var     goto    range   return      switch      continue
fallthrough     iota

iota

const (
        i = iota // 0 下一次递增1
        l
        a = "abc" // 空出
        z // 获取上面的值
        y
        x
    )

    fmt.Println(i, l, a, z, y, x) // 0 1 abc abc abc abc

保留字

// 内建常量:
true        false       iota        nil
// 内建类型:
int         int8        int16       int32       int64
uint        uint8       uint16      uint32      uint64      uintptr
float32     float64
complex128  complex64
bool:
byte        rune        string      error
// 内建函数:
make        delete      complex     panic       append      copy
close       len         cap         real        imag        new           recover

格式化

常用格式化字符如下:

%%    %字面量
%b    二进制整数值,基数为2,或者是一个科学记数法表示的指数为2的浮点数
%c    该值对应的unicode字符
%d    十进制数值,基数为10
%e    科学记数法e表示的浮点或者复数
%E    科学记数法E表示的浮点或者附属
%f    标准计数法表示的浮点或者附属
%o    8进制度
%p    十六进制表示的一个地址值
%s    输出字符串或字节数组
%T    输出值的类型,注意int32和int是两种不同的类型,编译器不会自动转换,需要类型转换。
%v    值的默认格式表示
%+v    类似%v,但输出结构体时会添加字段名
%#v    值的Go语法表示
%t    单词true或false
%q    该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x    表示为十六进制,使用a-f
%X    表示为十六进制,使用A-F
%U    表示为Unicode格式:U+1234,等价于"U+%04X"

实例

type User struct {
    Name string
    Age int
}
user : = User{
    "overnote",
    1,
}
fmt.Printf("%%\n")                   // %
fmt.Printf("%b\n", 16)               // 10000
fmt.Printf("%c\n", 65)               // A
fmt.Printf("%c\n", 0x4f60)           // 你
fmt.Printf("%U\n", '你')              // U+4f60
fmt.Printf("%x\n", '你')              // 4f60
fmt.Printf("%X\n", '你')              // 4F60
fmt.Printf("%d\n", 'A')              // 65
fmt.Printf("%t\n", 1 > 2)            // false
fmt.Printf("%e\n", 4396.7777777)     // 4.396778e+03 默认精度6位
fmt.Printf("%20.3e\n", 4396.7777777) //            4.397e+03 设置宽度20,精度3,宽度一般用于对齐
fmt.Printf("%E\n", 4396.7777777)     // 4.396778E+03
fmt.Printf("%f\n", 4396.7777777)     // 4396.777778
fmt.Printf("%o\n", 16)               // 20
fmt.Printf("%p\n", []int{1})         // 0xc000016110
fmt.Printf("Hello %s\n", "World")    // Hello World
fmt.Printf("Hello %q\n", "World")    // Hello "World"
fmt.Printf("%T\n", 3.0)              // float64
fmt.Printf("%v\n", user)             // {overnote 1}
fmt.Printf("%+v\n", user)            // {Name:overnote Age:1}
fmt.Printf("%#v\n", user)            // main.User{Name:"overnote", Age:1}

变量

声明变量

变量由:值、变量名、数据类型组成

var a int // 声明一个变量,默认为0
var b = 10 // 声明并初始化,且自动推导类型
c := 20 // 初始化,且自动推导


注意:

  • :=定义变量只能在函数内部使用,所以经常用var定义全局变量
  • Go对函数内已经声明但未使用的变量会在编译阶段报错:** not used
  • Go中的标识符以字母或者下划线开头,大小写敏感
  • Go推荐使用驼峰命名
  • Go定义变量后,数据类型不可更改,值可随意更改
  • Go中在同一个作用域,变量不可重名

一次声明多个变量

var a,b string
var a1,b1 string = "哼","哈"
var a2,b2 = 1,2   //类型可以直接省略
c,d := 1,2
var(
   e int
   f bool
)

_丢弃变量

_是个特殊的变量名,任何赋予它的值都会被丢弃。该变量不占用命名空间,也不会分配内存。

_, b := 34, 35      //将值35赋予b,并同时丢弃34

作用域

全局变量:在函数体外部定义
局部变量:在函数体内部定义

数据类型


值类型:int(整型),float(浮点型),bool(布尔型),string(字符串),array(数组),struct(结构体)
变量直接存储值,通常在内存的栈中存储

引用类型:point(指针),slice(切片),map(字典),func(函数),chan(管道),interface(接口)
变量存储的是一个地址,这个地址对应的空间存储了真正的值,在内存中通常分配在堆中,当没有任何变量引用(指向)这个地址时,这个地址对应的空间就变成了垃圾,由GC进行回收

int(整型)

整数类型有无符号(如int)和带符号(如uint)两种,这两种类型的长度相同,但具体长度取决于不同编译器的实现。
int8、int16、int32和int64四种有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,
同样uint8、uint16、uint32和uint64对应四种无符号整数类型。

有符号:

int     32位系统占4字节(与int32范围一样),64位系统占8个节(与int64范围一样)   
int8    占据1字节   范围 -128 ~ 127
int16   占据2字节   范围 -2(15次方) ~ 2(15次方)-1
int32   占据4字节   范围 -2(31次方) ~ 2(31次方)-1
int64   占据8字节   范围 -2(63次方) ~ 2(63次方)-1
rune    int32的别称

无符号:

uint    32位系统占4字节(与uint32范围一样),64位系统占8字节(与uint64范围一样)   
uint8   占据1字节   范围 0 ~ 255
uint16  占据2字节   范围 0 ~ 2(16次方)-1
uint32  占据4字节   范围 0 ~ 2(32次方)-1
uint64  占据8字节   范围 0 ~ 2(64次方)-1
byte    uint8的别称


注意:

  • 上述类型的变量由于是不同类型,不允许互相赋值或操作
  • Go默认的整型类型是int
  • 查看数据所占据的字节数方法:unsafe.Sizeof()

float(浮点型)

float32 单精度 占据4字节 范围 -3.403E38 ~ 3.403E38 (math.MaxFloat32)
float64 双精度 占据8字节 范围 -1.798E208 ~ 1.798E308 (math.MaxFloat64)

由上看出:

  • 浮点数是有符号的,浮点数在机器中存放形式是:浮点数=符号位+指数位+尾数位
  • 浮点型的范围是固定的,不受操作系统限制
  • .512 这样数可以识别为 0.512
  • 科学计数法:

    • 5.12E2 = 5.12 * 102
    • 5.12E-2 = 5.12 / 102

精度问题

var num1 float32 = -123.0000901
var num2 float64 = -123.0000901
fmt.Println("num1=",num1)        // -123.00009
fmt.Println("num2=",num2)        // -123.0000901

float32可以提供大约6个十进制数的精度,float64大约可以提供15个十进制的精度(一般选择float64)

相等判断问题

使用 == 号判断浮点数,是不可行的,替代方案如下:

package main

import (
    "fmt"
    "math"
)

func main() {
    var f1 float64 = 1.4
    var f2 float64 = 1.4
    rs := isEqual(f1, f2, 0.1)
    fmt.Println(rs) // true
}

func isEqual(f1,f2,p float64) bool {
    // p为用户自定义精度,如:0.00001
   return math.Abs(f1-f2) < p   
}

complex(复数)

Go中复数默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。
复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。

var t complex128
t = 2.1 + 3.14i
t1 = complex(2.1,3.14) // 结果同上
fmt.Println(real(t))   // 实部:2.1
fmt.Println(imag(t))   // 虚部:3.14

NaN(非数)

var z float64
// 输出 "0 -0 +Inf -Inf NaN"
fmt.Println(z, -z, 1/z, -1/z, z/z)

nan := math.NaN()
// "false false false"
fmt.Println(nan == nan, nan < nan, nan > nan)


注意:

  • 函数math.IsNaN用于测试一个数是否是非数NaN,
  • 函数math.NaN则返回非数对应的值。
  • 虽然可以用math.NaN来表示一个非法的结果,但是测试一个结果是否是非数NaN则是充满风险的,因为NaN和任何数都是不相等的。

string(字符串)

字符

Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存,且使用单引号包裹。

var c1 byte = 'a'
var c2 byte = '0'
fmt.Println("c1=", c1)                    //输出 97
fmt.Println("c2=", c2)                    //输出48
fmt.Printf("c1=%c,c2=%c\n", c1, c2)        //输出原值 a 0

//var c3 byte = '北'
//fmt.Printf("c3=%c", c3)                    // 溢出错误:overflows byte
//var b2 int = '山' // 中文用int存储
//fmt.Println(b2) // 23665

字符串

传统的字符串是由字符组成的,而Go的字符串是由单个字节连接起来的,即Go字符串是一串固定长度的字符连接起来的字符序列。
字符串在Go语言中是基本类型,内容在初始化后不能修改。
Go中的字符串都是采用UTF-8字符集编码,使用一对双引号""或反引号定义。 ` `可以原生输出源代码、防止攻击,即其没有字符转义功能。

var str1 string
str1 = "Hello "

// str1[0] = 'c'
// 字符串不可变,编译报错: cannot assign to 因为
fmt.Println(str1[0])         // 输出字符串第一个字符 72

str2 := `"hello", "world" \n`
fmt.Println(str2)         // 输出字符串  "hello", "world" \n

修改字符串

  • 通过转换为字节数组[]byte类型,构造一个临时字符串

    str := "hello"
    strTemp := []byte(str)
    fmt.Println("strTemp=", strTemp) // [104 101 108 108 111]
    strTemp[0] = 'c'
    strResult := string(strTemp)
    fmt.Println("strResult=", strResult)        // strResult= cello
  • 使用切片

    s := "hello"
    str := "c" + s[1:] 
    fmt.Println(str) // cello

字符串相关函数、包操作

len()函数是go语言的内建函数,可以用来获取字符串、切片、通道等的长度。

str := "hello"
fmt.Println(len(str)) // 5

str2 := "中国"
fmt.Println(len(str)) // 6 中文占3个字节
// 可以使用 unicode/utf8 包
fmt.Println(utf8.RuneCountInString(str2)) // 2

strings

var str = "!!!hello,中国,中文!!!"
fmt.Println(strings.Count(str, "l"))
// strings.Count(str, substr) 统计子字符串出现的次数
fmt.Println(strings.EqualFold("abc", "ABC")) 
// strings.EqualFold(str1, str2) 不区分大小写,对比字符是否相等

fmt.Println(strings.Index(str, "中")) 
//strings.Index(str, substr) 返回子字符串第一次出现的索引位置
fmt.Println(strings.LastIndex(str, "中文")) 
// strings.LastIndex(str, substr) 返回子字符串最后一次出现的索引位置

fmt.Println(strings.Replace(str, "中文", "你好", 15))  
// strings.Replace(str, old, new, index) 子字符串替换 index = -1 表示全部替换

fmt.Println(strings.Split(str, ",")) 
// strings.Split(str, substr) 根据子字符串切割,返回数组
fmt.Println(strings.ToUpper(str)) 
// strings.ToUpper(str) 英文字母转大写
fmt.Println(strings.ToLower(str)) 
// strings.ToLower(str) 英文字母转小写

fmt.Println(strings.TrimSpace(str)) 
// strings.TrimSpace(str) 剔除字符串两边的空格

fmt.Println(strings.Trim(str, "!")) 
// strings.Trim(str, cutstr) 剔除两边指定字符串
fmt.Println(strings.TrimLeft(str, "!")) 
// strings.TrimLeft(str, cutstr) 剔除左边指定字符串
fmt.Println(strings.TrimRight(str, "!")) 
// strings.TrimRight(str, cutstr) 剔除右边指定字符串

fmt.Println(strings.HasPrefix(str, "!")) 
// strings.HasPrefix(str, prefix) 验证字符串是否以指定字符串开头
fmt.Println(strings.HasSuffix(str, "!")) 
// strings.HasSuffix(str, suffix) 验证字符串是否以指定字符串结尾

字符串循环

var str = "hello"
for i := 0; i < len(str); i++ {
    fmt.Println(str[i])
}
// i 索引 ch 值
for i, ch := range str {
    fmt.Println(i, ch)
}

其他类型转字符串

string内置函数

num := 10
fmt.Printf("%T \n", string(num)) // string

fmt包提供了转换的方法fmt.Sprintf

num := 12
num2 := 12.3
bool := true
mChar := 's'
str1 := fmt.Sprintf("%d \n", num) // 整型转字符串
str2 := fmt.Sprintf("%f \n", num2) // 浮点型转字符串
str3 := fmt.Sprintf("%t \n", bool) // 布尔型转字符串
str4 := fmt.Sprintf("%c \n", mChar) // byte转字符串
fmt.Println(str1, str2, str3, str4)
fmt.Printf("%T \n", str1) // string
fmt.Printf("%T \n", str2) // string
fmt.Printf("%T \n", str3) // string
fmt.Printf("%T \n", str4) // string

strconv包Format系列函数

a := strconv.FormatBool(false) // 转布尔
b := strconv.FormatFloat(32.32, 'f', 1, 64) // 转浮点 值,fmt表示格式 f 浮点, g 科学计数,精度,进制
c := strconv.FormatInt(20, 10) // 转整型 值, 进制
d := strconv.Itoa(123) // 转int 值
fmt.Println(a, b)
fmt.Printf("%T \n", a)
fmt.Printf("%T \n", b)
fmt.Println(c, d)
fmt.Printf("%T \n", c)
fmt.Printf("%T \n", d)

字符串转其他类型

可以使用内置函数int()、float()等

字符串在转换其他类型时,需要注意值和转换类型要对应。

strconv包Parse系列函数

var str string = "123"
var str2 string = "123.321421"
i, _ := strconv.ParseInt(str, 10, 64) // 转int
j, _ := strconv.ParseFloat(str2, 64) // 转浮点
t, _ := strconv.ParseBool("false") // 转布尔
l, _ := strconv.Atoi("231")  // 转int

fmt.Println(i, j, t, l)
fmt.Printf("%T \n", i)
fmt.Printf("%T \n", j)
fmt.Printf("%T \n", t)
fmt.Printf("%T \n", l)

字符串拼接

使用+能够连接字符串。但是该操作并不高效(因为字符串在Go中是基本类型,每次拼接都是拷贝了内存!)。Go1.10提供了类似Java的StringBuilder机制来进行高效字符串连接:

str1 := "hello"
str2 := " world"

// 创建缓冲字节
var stringBuilder bytes.Buffer

// 字符串写入缓冲
stringBuilder.WriteString(str1)
stringBuilder.WriteString(str2)
fmt.Println(stringBuilder.String()) // hello world

strconv包Append系列函数

str := make([]byte, 0) //创建切片 类型, 大小
str = strconv.AppendInt(str, 100, 10) // 切片,数值,进制位
str = strconv.AppendBool(str, false)
str = strconv.AppendFloat(str, 5.5, 'f',3,64) // 切片,数值, 格式化类型, 小数位, 进制
fmt.Println(string(str)) // 切片转字符串

bool(布尔型)

布尔类型就是表示真或假,布尔类型取值只允许true和false,在Go语言中占用1个字节,布尔类型适用于逻辑运算。

var b bool = false

struct(结构体)

结构体可以用来声明新的类型,作为其他类型的属性/字段的容器。

type Person struct {
    name string
    age int
}

var stu = Person{"Sam", 18}
fmt.Println(stu.name) // 语法糖访问 Sam

指定赋值

stu2 := Person{age:19}
fmt.Println(stu2.age) // 19

申请构造体

stu := new(Person) // 返回的是指针类型
stu.name = "Sam"
fmt.Printf("%T \n", stu) // &{Sam 0}

结构体地址与实例化

前面说过,对结构体的new其实是生成了一个指针类型。其实对结构体进行&取地址操作时,也可以视为对该类型进行一次new的实例化操作。

内嵌构造体

type Person struct {
    name string
    age int
}

type Student struct {
    Person
    className string
}

stu := Student{
    Person{"Sam", 19},
    "on_1",
}

fmt.Println(stu.age, stu.name) // 19 Sam

array(数组)

数组是一段固定长度的连续内存区域。数组的长度定义后不可更改

var arr1 = [10]int{1} // 定义并初始化
arr2 := [...]int{1,2,3} // 自动计算长度
arr3 := [...]int{2:9,3:3} // 选择性初始化
fmt.Println(arr1,arr2, arr3)

常用操作

var a = arr1[5:] // 从索引4开始,获取后面的元素
var b = arr1[:5] // 到索引4结束,获取前面的元素
var c = arr1[:] // 获取全部的元素

fmt.Println(a, b, c, len(a)) // [0 0 0 0 0] [1 0 0 0 0] [1 0 0 0 0 0 0 0 0 0] 5
// 遍历数组
for i := 0; i < len(arr1); i++ {
    fmt.Println(arr1[i])
}

for i, v := range arr1 {
    fmt.Println(i, v)
}


注意:

  • 数组创建完长度就固定,不可以再追加元素;
  • 长度是数组类型的一部分,因此[3]int[4]int是不同的类型;
  • 数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该函数的副本,而不是他的指针。

slice(切片)

切片(slice)解决了数组长度不能扩展,以及基本类型数组传递时产生副本的问题。
与数组相比,切片多了一个存储能力值的概念,即元素个数与分配空间可以是两个不同的值

var s = []byte{1, 2} 
fmt.Println(s) // [1 2]

使用make函数

slice1 := make([]int,5)        // 创建长度为5,容量为5,初始值为0的切片
slice2 := make([]int,5,7)    // 创建长度为5,容量为7,初始值为0的切片
slice3 := []int{1,2,3,4,5}    // 创建长度为5,容量为5,并已经初始化的切片


注意

  • 通过数据源(数组)创建的切片,是对数据源的一个view(视图),如果修改切片中的元素,原数组的元素也会被修改。
  • 被修改的切片元素,和原数组的元素的值的地址是一致的。
  • 切片是可以向后拓展的,不可向前拓展,拓展的长度不能超过底层数组的cap

WX20200925-223540@2x.png

var s1 = make([]int, 5)
    var s2 = make([]int, 5)
    arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7}

    s1 = arr[2:6] // [2 3 4 5]

    s2 = s1[1:3] // [5 6]
    s2[0] = 100
    fmt.Printf("s1 = %v \n", s1) // s1 = [2 100 4 5]
    fmt.Printf("s2 = %v \n", s2) // s2 = [100 4]
    fmt.Printf("arr = %v \n", arr) // [0 1 2 100 4 5 6 7 0 0]

通过上面可以看出s1和s2都view同一个数据源,那么s2就可以取到arr后面所有的值。

package main

import "fmt"

func main() {


    var s1 = make([]int, 5)
    var s2 = make([]int, 5)
    arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 = arr[2:6] // [2 3 4 5]
    //s1 = arr[2:6:6] // [2 3 4 5] 可以避免 s2读取 arr后面的值
    
    s2 = s1[1:5] // [2 3 4 5 6]
    s2[0] = 100
    fmt.Printf("s1 = %v \n", s1) // s1 = [2 100 4 5]
    fmt.Printf("s2 = %v \n", s2) // s2 = [100 4 5 6]
    fmt.Printf("arr = %v \n", arr) // [0 1 2 100 4 5 6 7 0 0]
}

常用函数

cap()返回切片底层数组容量

var s = []byte{1, 2}
fmt.Println(cap(s)) // 2

append()增加切片元素

var s = []byte{1, 2}
s = append(s, 3, 1)
fmt.Println(s) // 2

copy()复制切片
默认0补位

var s = []byte{1, 2, 3}
s1 := make([]byte, 2)
num := copy(s1, s) // 返回元素个数 
fmt.Println(num, s1) // 2 [1 2]

切片的相关操作

遍历切片

s := make([]byte, 5)
for i, _ := range s{
    s[i] = byte(i) // 由于i是int类型 这里转成byte
}
fmt.Println(s)

删除切片元素

var s = []byte{1, 2, 3, 4}
i := 2
fmt.Println(s[:i]) // [1 2]
fmt.Println(s[i+1:]) // [4]
s = append(s[:i], s[i+1:]...) // ... 表示切片需要展开
fmt.Println(s) // [1 2 4]

切片作为参数

var s = []byte{1, 2, 3, 4}
test(s)
func test(s []byte) {
    fmt.Println("test---", s)
}

快排和二分查找

package sort

import "fmt"

func bubbleSort(arr []int) []int {
    var temp int
    // 冒泡
    for i := 0; i < len(arr); i++ {
        for j := 0; j < len(arr) - 1 - i; j++ {
            if arr[j] > arr[j + 1] {
                // 交换
                temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return arr
}

// 二分查找
func BinaryFind(arr *[]int,lIndex int, rIndex int, findNum int) int {
    if lIndex > rIndex {
        fmt.Println("未找到")
        return -1
    }
    // 计算中间下标
    var middle = (lIndex + rIndex) / 2
    if (*arr)[middle] > findNum { // 在左边
        middle = BinaryFind(arr, lIndex, middle - 1, findNum)
    } else if (*arr)[middle] < findNum {
        middle = BinaryFind(arr, middle + 1, rIndex, findNum)
    }else {
        if middle == 0 || (*arr)[middle-1] != findNum {
            return middle
        } else {
            middle = BinaryFind(arr, lIndex, middle - 1, findNum)
        }
    }
    return middle
}
// 快排
func QuickSort(arr *[]int,  lIndex int, rIndex int) {
    if lIndex >= rIndex {
        return
    }

    var pivot int = (*arr)[rIndex]
    var j = lIndex
    var temp int
    for i := lIndex; i < rIndex; i++ {
        if (*arr)[i] < pivot {
            (*arr)[j], (*arr)[i] = (*arr)[i], (*arr)[j]
            j++
        }
    }
    temp = (*arr)[j]
    (*arr)[j] = pivot
    (*arr)[rIndex] = temp
    QuickSort(arr, lIndex, j - 1)
    QuickSort(arr, j + 1, rIndex)
    return
}

point(指针)

指针代表某个内存地址,通过 * 定义指针,赋值时,使用 & 取变量地址,默认值为 nil

var a = "abc"
var p *string = &a
fmt.Println(p) // 0xc000010200 地址
fmt.Println(*p) // abc


注意:

  • Go同样支持多级指针,如 **T
  • 空指针:声明但未初始化的指针
  • 野指针:引用了无效地址的指针,如:var p *int = 0var p *int = 0xff00(超出范围)
  • Go不支持指针运算,由于垃圾回收机制的存在,指针运算造成许多困扰,所以Go直接禁止了指针运算

指针相关操作

通过指针修改变量的值

var a = "abc"
var p *string = &a
*p = "ddd" // 修改a的值 等价于 a = "ddd"
fmt.Println(a) // ddd

new()函数可以在 heap堆 区申请一片内存地址空间

var p *bool
p = new(bool)
fmt.Println(p) // 0xc00001c082
fmt.Println(*p) // false

map(集合)

map集合是一个无序键值对组合,也被称为字典。

m := map[string]int{"a":1, "b":2}
fmt.Println(m["a"]) // 1
// make方式
m := make(map[string]int)
m["a"] = 1
fmt.Println(m) // map[a:1]


注意:

  • golang中map的 key 通常 key 为 int 、string,但也可以是其他类型如:bool、数字、string、指针、channel,还可以是只包含前面几个类型的接口、结构体、数组。
  • slice、map、function由于不能使用 == 来判断,不能作为map的key。
  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取;
  • map的长度是不固定的,也就是和slice一样,也是一种引用类型
  • 内置的len函数同样适用于map,返回map拥有的key的数量
  • go没有提供清空元素的方法,可以重新make一个新的map,不用担心垃圾回收的效率,因为go中并行垃圾回收效率比写一个清空函数高效很多
  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制

type Person struct {
    name string
    age int
}

m := map[string]Person{"key" : {"sam", 18}}
fmt.Println(m) // map[key:{sam 18}]
fmt.Println(m["key"].name) // sam

p, j := m["key"] // 返回两个值,1:当前key的值,2:key是否存在的bool值
fmt.Println(p) // {sam 18} key 不存在返回默认值 { 0}
fmt.Println(j) // true key 不存在返回false

集合相关操作
遍历集合

for i, v := range m {
    fmt.Println(i, v)
}

增加元素

m["no"] = Person{"alpha", 12}

删除某个key

delete(m, "key")

sync.Map

Go内置的map只有读是线程安全的,读写是线程不安全的。
需要并发读写时,一般都是加锁,但是这样做性能不高,在go1.9版本中提供了更高效并发安全的sync.Map。


sync.Map的特点:

  • 无须初始化,直接声明即可
  • sync.Map不能使用map的方式进行取值和设值操作,而是使用sync.Map的方法进行调用。Store表示存储,Load表示获取,Delete表示删除。
  • 使用Range配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,需要继续迭代时,返回true,终止迭代返回false。

var m sync.Map
m.Store("key", 10) // 增加元素
fmt.Println(m.Load("key")) // 10 true 获取元素 返回两个值,key对应的值, key是否存在的值
// 遍历
m.Range(func (k, v interface{}) bool{
    fmt.Println(k, v)
    return true
})

func(函数)

函数也可以称之为方法,在针对相同操作的代码块,将其封装起来,其他需要的地方进行调用。

func 函数名字 (参数列表) (返回值列表){
    // 函数体
    return 返回值列表
}


注意:

  • 函数名首字母小写为私有,大写为公有;
  • 参数列表可以有0-多个,多参数使用逗号分隔,不支持默认参数;
  • 返回值列表返回值类型可以不用写变量名
  • 如果只有一个返回值且不声明类型,可以省略返回值列表与括号
  • 如果有返回值,函数内必须有return
//无返回值,默认返回0,所以也可以写为 func fn() int {}
func fn(){}  

//Go推荐给函数返回值起一个变量名
func fn1() (result int) {
    return 1
}

//第二种返回值写法
func fn2() (result int) {
    result = 1
    return 
}

//多返回值情
func fn3() (int, int, int) {
   return 1,2,3
}

//Go返回值推荐多返回值写法:
func fn4() (a int, b int, c int) {        多个参数类型如果相同,可以简写为: a,b int
   a , b, c = 1, 2, 3
   return 
}

值传递和引用传递

不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的 数据大小,数据越大,效率越低。

如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。

可变参数

package main

import (
    "fmt"
    "bytes"
)

func main () {
    fmt.Println(joinString("hello", " world", "!!!!", "~~~~"))

}
// 表示参数可无限延伸,类型是字符串
func joinStrings(s ...string) string {
    var buf bytes.Buffer

    for _, v := range s {
        buf.WriteString(v)
    }

    return buf.String()
}


注意
可变参数必须放在形参列表的最后

匿名函数

定义

s := "hello"
func (str string) {
    fmt.Println(s)
}(s) // 直接调用,不希望直接调用可以用变量接收


f1 := func (str string) {
    fmt.Println(s)
}
f1(s)

案例

i := 10
x := 1
max, min := func(a, b int) (max, min int) {
    if a > b {
        max = a
        min = b
    } else {
        max = b
        min = a
    }
    return
}(i, x)

fmt.Println(max, min)

闭包

闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使己经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。

案例:

str := "hello"
foo := func(){            // 声明一个匿名函数
    str = "world"
}
foo()                //  调用匿名函数,修改str值
fmt.Print(str)            // world
package main

import (
    "fmt"
    "strings"
)

func makeSuffix(suffix string) func (string) string {
    return func (str string) string {
        if strings.HasSuffix(str, suffix) {
            return str
        }
        str += ".jpg"
        return str

    }

}

func main() {
    var a = "a.jpg"
    var b = "b"
    f := makeSuffix(".jpg")
    fmt.Println(f(a)) // 
    fmt.Println(f(b))
}

特殊函数

  • main()Go语言中每个包的入口函数,编译过的包或者run执行某个包文件,执行的都是该函数中的代码。
  • init()Go语言中包内定义这个函数是用来初始化,当一个包进行初始化完毕后会自动执行该函数,比main()方法的执行优先级要高,每个包文件只能有一个init()方法
  • new函数可以用来创建变量。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型
  • make()函数经常用来创建切片、Map、管道


注意:
new只是一个预定义函数,并不是一个关键字,所以new也有可能会被项目定义为别的类型。

排序

// 冒泡排序
func bubbleSort(arr [5]int) [5]int {
    var temp int
    // 冒泡
    for i := 0; i < len(arr); i++ {
        for j := 0; j < len(arr) - 1 - i; j++ {
            if arr[j] > arr[j + 1] {
                // 交换
                temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return arr
}
// 快排
func quickSort(arr [5]int,  lIndex int, rIndex int) [5]int {
    if lIndex >= rIndex {
        return arr
    }

    var pivot int = arr[rIndex]
    var j = lIndex
    var temp int
    for i := lIndex; i < rIndex; i++ {
        if arr[i] < pivot {
            temp = arr[j]
            arr[j] = arr[i]
            arr[i] = temp
            j++
        }
    }
    temp = arr[j]
    arr[j] = pivot
    arr[rIndex] = temp
    arr = quickSort(arr, lIndex, j - 1)
    arr = quickSort(arr, j + 1, rIndex)
    return arr
}

二分查找

func binaryFind(arr [5]int,lIndex int, rIndex int, findNum int) int {

    if lIndex > rIndex {
        fmt.Println("未找到")
        return -1
    }
    // 计算中间下标
    var middle = (lIndex + rIndex) / 2
    if arr[middle] > findNum { // 在左边
        middle = binaryFind(arr, lIndex, middle - 1, findNum)
    }

    if arr[middle] < findNum {
        middle = binaryFind(arr, middle + 1, rIndex, findNum)
    }
    return middle
}

自定义类型

类型别名
Go在1.9版本加入了类型别名。主要用于代码升级、迁移中类型的兼容问题(C/C++中使用宏来解决重构升级带来的问题)。

Go1.9之前的版本内部定义了新的类型byte和rune,用于指代uint8int32

type MyInt int                          // 类型定义
type AliasInt = int                     // 类型别名,支持使用括号,同时起多个别名

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

var a2 AliasInt
fmt.Printf("a2 type: %T\n", a2)            //int


注意
自定义类型与原类型不是同一类型

零值机制

Go变量初始化会自带默认值,不像其他语言为空,下面列出各种数据类型对应的0值:

  • int 0
  • int8 0
  • int32 0
  • int64 0
  • uint 0x0
  • rune 0 // rune的实际类型是 int32
  • byte 0x0 // byte的实际类型是 uint8
  • float32 0 // 长度为 4 byte
  • float64 0 // 长度为 8 byte
  • bool false
  • string ""

类型转换

类型转换上Go和Java、C,Go在不同类型的变量之间赋值时需要显示转换,既Go中的数据类型不能进行自动转换。

  • int()
  • int8()
  • int32()
  • int64()
  • float32()
  • float64()
  • string()
var i float32 = 10.3
b := int(i)
fmt.Println(b)


注意:

  • Go中类型转换对表示范围大小没有要求
  • 被转换的是变量存储的数据(既值),变量本身的数据类型并没有变化
  • 在转换中,如果将进制位大的转换为进制位小的,编译时不会报错,只是转换结果按照溢出处理,得到的结果和我们希望的不一致,因此,转换时需要考虑范围
var i int = 65536
b := int8(i) // 溢出处理
fmt.Println(b) // 0

运算符

Go中没有三元运算符,Go希望使用条件控制语句来进行运算

算术运算符:    +    -    *    /    %    ++    --
关系运算符:    ==    !=    <=    >=    <    >
逻辑运算符:    !    &&    ||
位运算:        &(按位与)    |(按位或)    ^(按位取反)    <<(左移)    >>(右移)
赋值运算符:    =    +=    -=    *=    /=    %=    <<=    >>=    &=    ^=    |=
其他运算符:    &(取地址)    *(取指针值) <-(Go Channel相关运算符)

优先级

var c float64 = 10.0 / 3
fmt.Println(c)

自增&&自减

Go中只有后--后++,且自增自减不能用于表达式中,只能独立使用:

a = i++           // 错误用法
if i++ > 0 {}     // 错误用法
i++               // 正确用法

位运算

&     按位与,参与运算的两个数二进制位相与:同时为1,结果为1,否则为0
|     按位或,参与运算的两个数二进制位相或:有一个为1,结果为1,否则为0
^     按位异或:二进位不同,结果为1,否则为0
<<    按位左移:二进位左移若干位,高位丢弃,低位补0,左移n位其实就是乘以2的n次方
>>    按位右移:二进位右移若干位,右移n位其实就是除以2的n次方

流程控制

程序在执行时,会按照代码一行一行的执行。流程控制即改变程序运行顺序的指令,有时可能需要满足一定条件才可执行其中的代码块或需要重复执行某一代码块。

条件控制

if

// 初始化与判断写在一起: if a := 10; a > 10
if a := 10; a > 10 {
    fmt.Println("大于10")
} else {
    fmt.Println("不大于10")
}

switch

num := 10
switch num {
    case 5:
        fmt.Println("5")
        break
    case 10:
        fmt.Println("10")
        break
    default:
        break
}

num := 10
switch num {
    case 5:
        fmt.Println("5")
        break
    case 10:
        fmt.Println("10")
        fallthrough
    default:
        fmt.Println("20")
        break
}
// 输出10  20

循环控制

for

// 传统的for循环
for init;condition;post{
}

// for循环简化
var i int
for ; ; i++ {
   if(i > 10){
      break;
   }
}

// 类似while循环
for condition {}

// 死循环
for{
}

// for range:一般用于遍历数组、切片、字符串、map、管道
for k, v := range []int{1,2,3} {
}

跳出循环

常用的跳出循环关键字:

  • break用于函数内跳出当前forswitchselect语句的执行
  • continue用于跳出for循环的本次迭代。
  • goto可以退出多层循环
// continue
num := 10
for ; num > 5; num-- {
    if num == 8 {
        continue
    }
    fmt.Println(num)
}
// goto
num := 10
for ; num > 5; num-- {
    if num == 8 {
        goto here
    }
    fmt.Println(num)
}

here:
    fmt.Println("goto there")

常量

常量:在代码编译时就定义好的值,运行过程中不可进行修改

定义方式:

const A = 3
const PI float32 = 3.1415
const mask = 1 << 3                        //常量与表达式
错误写法:常量赋值是一个编译期行为,右边的值不能出现在运行时才能得到结果的值。
const HOME = os.GetEnv("HOME")

错误处理

go中不支持try{}catch(){},采用defer,panic(),recover()来处理异常。

defer(延时机制)

在程序中经常需要创建资源,比如数据库连接、文件句柄等,go中采用延时机制。

defer fmt.Println("defer_1")
defer fmt.Println("defer_2")
fmt.Println("3")
// 输出 3 defer_2 defer_1
  • 当执行到defer时,不会立即执行,会将后面的语句压入 defer栈。
  • 程序执行完毕时,会以先入后出的顺序执行。

panic()和recover()

  • panic:用来抛出异常,抛出后,后面的代码块不会执行
  • recover:用来捕获异常

举个栗子

func test() {
    defer func() {
        error := recover()
        if error != nil {
            fmt.Println(error)
        }
    }()
    panic("抛出异常") // recover()可捕获该异常
    fmt.Println("ok") // 不会执行
}
func main() {
    test()
    fmt.Println(11) // 会执行
}

自定义错误

package main

import (
    "errors"
    "fmt"
)

func test(i int) (err error) {
    if i == 10 {
        return nil
    } else {
        return errors.New("创建一个错误")
    }
}

func test2() {
    defer func() {
        errs := recover()
        if errs != nil {
            fmt.Println(errs)
        }
    }()
    err := test(1)
    if err != nil {
        panic(err) // recover()可捕获该异常
    }
    fmt.Println("ok") // 不会被执行
}
func main() {
    test2()
    fmt.Println(11) // 会执行
}

goroutine(协程)

协程:程序在执行时,函数内部可以中断,适当时候返回接着执行,即协程运行在用户态
main主函数也是一个协程

goroutine相关API位于 runtime 包。

如果Go程序要运行在多核上,则可以如下操作,此时可以实现程序的并行执行:

cpuNum := runtime.NumCPU() // 获取当前系统的CPU核心数
runtime.GOMAXPROCS(cpuNum) // Go中可以轻松控制使用核心数

 // 在Go1.5之后,程序已经默认运行在多核上,无需上述设置

runtime.Gosched()  // 用于让出CPU时间片,即让出当前协程的执行权限,调度器可以去安排其他等待的任务运行。
runtime.Goexit() // 用于立即终止当前协程运行,调度器会确保所有已注册defer延迟调用被执行。
package main

import (
    "bytes"
    "fmt"
    "os"
    "strconv"
    "time"
)

func test(id int) {
    fmt.Println("开始写入")
    f, _ := os.OpenFile("ids.log", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
    defer f.Close()
    var stringBuilder bytes.Buffer
    stringBuilder.WriteString("---start-- \n")
    stringBuilder.WriteString(strconv.Itoa(id))
    stringBuilder.WriteString("\n")
    stringBuilder.WriteString("---end-- \n")
    str := stringBuilder.String()
    fmt.Println(str)
    res, err := f.WriteString(str)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("结束写入", res)

}

func main() {

    for i := 1; i <= 10; i++ { // 并发或并行执行
        go test(i)
    }
    time.Sleep(time.Second) // 防止主线程结束时,协程未结束
}


注意
上述代码是并发或者并行执行的,所以没法保证写入顺序

MPG理解

Go的线程实现模型有三个元素,即MPG:

  • M:machine,一个M代表一个工作线程
  • P:processor,一个P代表执行一个Go代码段需要的上下文环境
  • G:goroutine,一个G代表一个Go协程

WX20200924-143642@2x.png

WX20200924-151312@2x.png

互斥锁

互斥锁可以解决资源争夺问题

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    myMap = make(map[int]int, 10)

    // lock 是全局互斥锁
    // sync 是包 同步
    // Mutex 互斥

    lock sync.Mutex
)

func tests(n int) {

    res := 1
    for i := 1; i <=n; i++ {
        res *= i
    }
    lock.Lock() // 加锁
    myMap[n] = res // concurrent map writes
    lock.Unlock() // 解锁
}

func main() {
    for x := 1; x <= 200; x++ {
        go tests(x)
    }
    // 防止主线程结束时,协程未结束
    time.Sleep(time.Second * 20)

    lock.Lock()
    for i, v := range myMap {
        fmt.Printf("map[%d]%d \n", i, v)
    }
    lock.Unlock()
}

channel(管道)

go提供了一个channel(管道)数据类型,可以解决协程之间的通信问题!channel的本质是一个队列,遵循先进先出规则(FIFO),内部实现了同步,确保了并发安全!

package main

import "fmt"

func main() {
    var c = make(chan int, 2)

    fmt.Printf("%T  %v \n", c, c)

    c<-10
    n := 100
    c<-n
    //c<-n // 超过长度时再进行插入 deadlock err
    fmt.Println(len(c))
    num1 := <-c
    num2 := <-c
    //num3 := <-c // 全部取出后再进行取出 deadlock err

    fmt.Println(num1, num2, len(c))
}


注意

  • 一旦指定管道长度,数据满了以后就不能再插入
  • 一旦管道数据取完了,既不能再取出
  • 只能存放指定的数据类型

管道相关操作

**只读和只写

var chan1 chan<- int        // 声明 只写channel
var chan2 <-chan int         // 声明 只读channel

阻塞

package main

import (
    "fmt"
    "time"
)

func write(ch chan int) {
    ch<-10
    fmt.Printf("%v \n", ch) // 输出10
    ch<-20
    fmt.Printf("%v \n", ch) // 输出管道地址
    ch<-30 // 该处数据未读取,后续操作直接阻塞
    fmt.Printf("%v \n", ch) // 没有输出
}

func read(ch chan int) {
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
func main() {
    var ch = make(chan int) // 无缓冲管道
    // 写入
    go write(ch)
    // 读取
    go read(ch)
    
    time.Sleep(time.Second*3)
}

接口与管道

package main

import "fmt"

type Person struct {
    Name string
    Age int
}

func main() {
    var ch = make(chan interface{}, 10) // 有缓冲管道

    
    ch<-Person{"Sam", 19}
    ch<-Person{"Kobe", 19}
    ch<-10

    sam := <-ch
    fmt.Printf("%T   %v \n", sam, sam)
    fmt.Printf("%T   %v \n", sam, sam.(Person).Name)

}

遍历管道

package main

import (
    "fmt"
    "math/rand"
    "strconv"
)

type Person struct {
    Name string
    Age int
    Address string
}

func main() {
    var ch = make(chan Person, 10)

    for i := 0; i < 10; i++ {
        p := Person{
            Name : "Sam" + strconv.Itoa(i),
            Age: rand.Intn(100),
            Address: "Address" + strconv.Itoa(i),
        }
        ch<-p
    }
    close(ch) // 遍历前需要关闭管道,否则报 deadlock
    for v := range ch {
        fmt.Println(v)
    }
}

反射

反射可以在运行时动态获取变量的各种信息,比如:类型(type),类别(kind),通过反射可以修改变量的值,如果变量是结构体,还可以调用其方法。


注意

  • 编译期间,反射代码的一些错误无法进行提示
  • 反射影响性能

reflect

go反射相关包reflect

面向对象

go的面向对象编程和其他语言有很大区别,go中没有构造函数、析构函数、继承、接口实现等,也没有公共、受保护、私有这类关键字的概念。

定义类

可以借助结构体来定义类

// 定义一个Person类 成员方法say,run
type Person struct {
    name string
    age int

}
func (p Person) say() {
    fmt.Println(p.name, "say: hello")
}
func (p Person) run() {
    fmt.Println(p.name, "running!")
}

func main() {
    p := Person{"Sam", 18}
    p.say()
    p.run()
}

为其他类型增加成员方法

type Str string

func (s Str) say(str string) {
    fmt.Println(str)
}
func main() {
    var str Str
    str.say("hello")

}

继承和重写

type Student struct {
    Person
    class string
}

func (stu Student) say() {
    fmt.Println(stu.name, "say: ok")
}


注意

  • 定义类时,外部可访问的方法名首字母需要大写,变量同理,官方术语表示:该方法变量可导出。
  • 小写方法变量只能在本包内进行访问。
  • 同一目录下类名尽量不要重复,方法名不能重复。

interface(接口)

package main

import (
    "fmt"
)

type Log interface {
    Writer()
    Reader()
}

type LogPrint interface {
    Log
    Print()
}

type Database struct {

}

func (db *Database) Writer() {
    fmt.Println("log writer in database")
}

func (db *Database) Reader() {
    fmt.Println("read log to database")
}

func (db *Database) Print() {
    fmt.Println("print log to database")
}

type File struct {

}

func (f *File) Writer() {
    fmt.Println("log writer in file")
}

func (f *File) Reader() {
    fmt.Println("read log to file")
}

func (f *File) Print() {
    fmt.Println("print log to file")
}

type Program struct {

}

// 接收一个接口类型
// 就是实现了 usb 接口的所有方法的 类
func (p *Program) Working(log Log) {
    log.Writer()
    log.Reader()
}

func main() {
    //p := Program{}
    db := Database{}
    file := File{}

    //p.Working(&db)
    //p.Working(&file)

    // 直接调用
    //var log Log = &db
    var logp LogPrint = &db
    logp.Writer()
    logp.Reader()
    logp.Print()

    //log = &file
    logp = &file
    logp.Writer()
    logp.Reader()
    logp.Print()
}


注意

  • 接口可以定义一些方法不需要实现这些方法。
  • 接口内部也不能出现变量。
  • 与其他语言不同的是go同时可以实现多个接口。(不是显示实现,而是以方法为基准)
  • 接口也可以存在继承关系。
  • 空接口没有任何方法,所以其他类型也可以实现接口(空接口可以接收任意类型的变量)。
  • 接口是引用类型。

package main

import "fmt"

type AInterface interface {
    Say()
}
type BInterface interface {
    Hello()
}
type Integer int

func (i Integer) Say() {
    fmt.Println("integer = ", i)
}
func (i Integer) Hello() {
    fmt.Println("hello")
}
func main() {

    var i Integer = 10

    var ai AInterface = i
    var bi BInterface = i
    ai.Say()
    bi.Hello()
}

类型断言

由于接口是一般类型,不知道具体类型,如果需要转成具体类型,要使用类型断言

func (p Program) Working(log Log) {
    log.Writer()

    // 类型断言
    if y, e := log.(Database); e {
        y.Delete()
    }

    log.Reader()
}

package(包)

go是通过包进行管理的,可以将person类封装成一个包。
└── src
├── main
│ ├── main.go
├── person
│ └── person.go

在src文件夹下建立person文件夹和person.go文件。

package person

import "fmt"

type Person struct {
    name string
    age int

}
func NewPerson(name string, age int) *Person {
    return &Person{name, age}
}
func (p Person) Say() {
    fmt.Println(p.name, "say: hello")
}
func (p Person) Run() {
    fmt.Println(p.name, "running!")
}
package main

import (
    "person"
)
func main() {
    p := person.NewPerson("Sam", 18)
    p.Say()
    p.Run()
}

编译包

生成可执行文件

go build

目录下需要有main包

  • -i:包内引入的其他包同时编译为库文件
  • -o:配置包文件名称、路径

go install

无main包的情况下会生成($HOME/pkg)库文件,有main包的情况下会生成可执行文件和库文件

修改结构体的值

通过指针修改

func (p *Person) SetAge (age int) {
    p.age = age
}
最后修改:2020 年 09 月 27 日 10 : 45 AM
如果觉得我的文章对你有用,请随意赞赏