Go开发环境

Go语言的安装

IDE的安装

  • Goland/idea+Go插件
  • Vscode

Go基础

Go语言结构

Go 语言的基础组成有以下几个部分:

  • 包声明
  • 引入包
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释

基础语法

Go 标记

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:

fmt.Println("Hello, World!")

6 个标记是(每行一个):

fmt
.
Println
(
"Hello, World!"
)

行分隔符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

以下为两个语句:

fmt.Println("Hello, World!")
fmt.Println("go基础")

标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符不能是数字,只能是字母或下划线。

以下是有效的标识符:

mahesh   kumar   abc   move_name   a_123
myname50   _temp   j   a23b9   retVal

以下是无效的标识符:

  • 1ab(以数字开头)
  • case(Go 语言的关键字)
  • a+b(运算符是不允许的)

关键字

下面列举了 Go 代码中会使用到的 25 个关键字或保留字:

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

除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符:

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

程序一般由关键字、常量、变量、运算符、类型和函数组成。

程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。

程序中可能会使用到这些标点符号:.、,、;、: 和 …。


Go 语言的空格

Go 语言中变量的声明必须使用空格隔开,如:

var age int

语句中适当使用空格能让程序看易阅读。

无空格:

fruit=apples+oranges

在变量与运算符间加入空格,程序看起来更加美观,如:

fruit = apples + oranges

数据类型

在 Go 编程语言中,数据类型用于声明函数和变量。

数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。

Go 语言按类别有以下几种数据类型:

序号 类型和描述
1 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。
2 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
3 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
4 派生类型: 包括:(a) 指针类型(Pointer)(b) 数组类型© 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型

变量

标准声明

Go语言的变量声明格式为:

    var 变量名 变量类型

变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

    var name string
    var age int
    var isOk bool

批量声明

每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明:

    var (
        a string
        b int
        c bool
        d float32
    )

变量的初始化

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

    var 变量名 类型 = 表达式

举个例子:

    var name string = "pprof.cn"
    var sex int = 1

或者一次初始化多个变量

    var name, sex = "pprof.cn", 1

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

    const pi = 3.1415
    const e = 2.7182

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明:

    const (
        pi = 3.1415
        e = 2.7182
    )

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:

    const (
        n1 = 100
        n2
        n3
    )

上面示例中,常量n1、n2、n3的值都是100

运算符

Go 语言内置的运算符有:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符

算数运算符

运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符。


关系运算符

运算符 描述
== 检查两个值是否相等,如果相等返回 True 否则返回 False。
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。

逻辑运算符

运算符 描述
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
ll 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
! 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。

位运算符

位运算符对整数在内存中的二进制位进行操作。

运算符 描述
& 参与运算的两数各对应的二进位相与。(两位均为1才为1)
l 参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
<< 左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>> 右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。

赋值运算符

运算符 描述
= 简单的赋值运算符,将一个表达式的值赋给一个左值
+= 相加后再赋值
-= 相减后再赋值
*= 相乘后再赋值
/= 相除后再赋值
%= 求余后再赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
l= 按位或后赋值
^= 按位异或后赋值

条件语句

Go 语言提供了以下几种条件判断语句:

语句 描述
if 语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。
if…else 语句 if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。
if 嵌套语句 你可以在 ifelse if 语句中嵌入一个或多个 ifelse if 语句。
switch 语句 switch 语句用于基于不同条件执行不同动作。
select 语句 select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。

循环语句

Go 语言循环语句

Go 语言提供了以下几种类型循环处理语句:

循环类型 描述
for 循环 重复执行语句块
循环嵌套 在 for 循环中嵌套一个或多个 for 循环

循环控制语句

循环控制语句可以控制循环体内语句的执行过程。

GO 语言支持以下几种循环控制语句:

控制语句 描述
break 语句 经常用于中断当前 for 循环或跳出 switch 语句
continue 语句 跳过当前循环的剩余语句,然后继续进行下一轮循环。
goto 语句 将控制转移到被标记的语句。

函数

函数定义

Go 语言函数定义格式如下:

func function_name( [parameter list] ) [return_types] {
   函数体
}

函数定义解析:

  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。

实例

以下实例为 max() 函数的代码,该函数传入两个整型参数 num1 和 num2,并返回这两个参数的最大值:

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
  /* 声明局部变量 */
  var result int

  if num1 > num2 {
   result = num1
  } else {
   result = num2
  }
  return result
}

函数调用

当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。

调用函数,向函数传递参数,并返回值,例如:

实例

package main

import "fmt"

func main() {
  /* 定义局部变量 */
  var a int = 100
  var b int = 200
  var ret int

  /* 调用函数并返回最大值 */
  ret = max(a, b)

  fmt.Printf( "最大值是 : %d\n", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
  /* 定义局部变量 */
  var result int

  if (num1 > num2) {
   result = num1
  } else {
   result = num2
  }
  return result
}

以上实例在 main() 函数中调用 max()函数,执行结果为:

最大值是 : 200

数组

声明数组

语法格式如下:

var a [len]int

数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。

var a [5]int

初始化数组

以下演示了数组初始化:

var arr0 [5]int = [5]int{1, 2, 3}

我们也可以通过字面量在声明数组的同时快速初始化数组:

arr0 := [5]int{1, 2, 3} 

如果数组长度不确定,可以使用 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

var b = [...]int{1, 2, 3, 4} 
或
b := [...]int{1, 2, 3, 4} 

如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

//  将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

 balance[4] = 50.0

以上实例读取了第五个元素。数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。

img


访问数组元素

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如:

var salary float32 = balance[9]

以上实例读取了数组 balance 第 10 个元素的值。

以下演示了数组完整操作(声明、赋值、访问)的实例:

实例

package main

import "fmt"

func main() {
  var n [10]int */\* n 是一个长度为 10 的数组 \*/*
  var i,j int

  /* 为数组 n 初始化元素 */    
  for i = 0; i < 10; i++ {
   n[i] = i + 100 /* 设置元素为 i + 100 */
  }

  /* 输出每个数组元素的值 */
  for j = 0; j < 10; j++ {
   fmt.Printf("Element[%d] = %d\n", j, n[j] )
  }
}

以上实例执行结果如下:

Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109

切片

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

  1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
  2. 切片的长度可以改变,因此,切片是一个可变的数组。
  3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
  4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
  5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int。
  6. 如果 slice == nil,那么 len、cap 结果都等于 0。

定义切片

你可以声明一个未指定大小的数组来定义切片:

var identifier []type

切片不需要说明长度。

或使用 make() 函数来创建切片:

var slice1 []type = make([]type, len)

也可以简写为

slice1 := make([]type, len)

也可以指定容量,其中 capacity 为可选参数。

make([]T, length, capacity)

这里 len 是数组的长度并且也是切片的初始长度。

切片初始化

s :=[] int {1,2,3 } 

直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

s := arr[:] 

初始化切片 s,是数组 arr 的引用。

s := arr[startIndex:endIndex] 

将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。

s := arr[startIndex:] 

默认 endIndex 时将表示一直到arr的最后一个元素。

s := arr[:endIndex] 

默认 startIndex 时将表示从 arr 的第一个元素开始。

s1 := s[startIndex:endIndex] 

通过切片 s 初始化切片 s1。

s :=make([]int,len,cap) 

通过内置函数 make() 初始化切片s[]int 标识为其元素类型为 int 的切片。


len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

以下为具体实例:

实例

package main

import "fmt"

func main() {
  var numbers = make([]int,3,5)

  printSlice(numbers)
}

func printSlice(x []int){
  fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

以上实例运行输出结果为:

len=3 cap=5 slice=[0 0 0]

空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

实例

package main

import "fmt"

func main() {
  var numbers []int

  printSlice(numbers)

  if(numbers == nil){
   fmt.Printf("切片是空的")
  }
}

func printSlice(x []int){
  fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

以上实例运行输出结果为:

len=0 cap=0 slice=[]
切片是空的

切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

实例

package main

import "fmt"

func main() {
  /* 创建切片 */
  numbers := []int{0,1,2,3,4,5,6,7,8} 
  printSlice(numbers)

  /* 打印原始切片 */
  fmt.Println("numbers ==", numbers)

  /* 打印子切片从索引1(包含) 到索引4(不包含)*/
  fmt.Println("numbers[1:4] ==", numbers[1:4])

  /* 默认下限为 0*/
  fmt.Println("numbers[:3] ==", numbers[:3])

  /* 默认上限为 len(s)*/
  fmt.Println("numbers[4:] ==", numbers[4:])

  numbers1 := make([]int,0,5)
  printSlice(numbers1)

  /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
  number2 := numbers[:2]
  printSlice(number2)

  /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
  number3 := numbers[2:5]
  printSlice(number3)

}

func printSlice(x []int){
  fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

执行以上代码输出结果为:

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

实例

package main

import "fmt"

func main() {
  var numbers []int
  printSlice(numbers)

  /* 允许追加空切片 */
  numbers = append(numbers, 0)
  printSlice(numbers)

  /* 向切片添加一个元素 */
  numbers = append(numbers, 1)
  printSlice(numbers)

  /* 同时添加多个元素 */
  numbers = append(numbers, 2,3,4)
  printSlice(numbers)

  /* 创建切片 numbers1 是之前切片的两倍容量*/
  numbers1 := make([]int, len(numbers), (cap(numbers))*2)

  /* 拷贝 numbers 的内容到 numbers1 */
  copy(numbers1,numbers)
  printSlice(numbers1) 
}

func printSlice(x []int){
  fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

以上代码执行输出结果为:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

指针

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

以下实例演示了变量在内存中地址:

package main

import "fmt"

func main() {
   var a int = 10   

   fmt.Printf("变量的地址: %x\n", &a  )
}

执行以上代码输出结果为:

变量的地址: 20818a220

现在我们已经了解了什么是内存地址和如何去访问它。接下来我们将具体介绍指针。


什么是指针

一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址。

类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

本例中这是一个指向 int 和 float32 的指针。


如何使用指针

指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值。

在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

package main

import "fmt"

func main() {
   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */

   ip = &a  /* 指针变量的存储地址 */

   fmt.Printf("a 变量的地址是: %x\n", &a  )

   /* 指针变量的存储地址 */
   fmt.Printf("ip 变量的存储地址: %x\n", ip )

   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}

以上实例执行输出结果为:

a 变量的地址是: 20818a220
ip 变量的存储地址: 20818a220
*ip 变量的值: 20

Go 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

查看以下实例:

package main

import "fmt"

func main() {
   var  ptr *int
   fmt.Printf("ptr 的值为 : %v\n", ptr  )
   fmt.Printf("ptr 的值为 : %#v\n", ptr  )

}

以上实例输出结果为:

ptr 的值为 : <nil>
ptr 的值为 : (*int)(nil)

空指针判断:

if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil)    /* ptr 是空指针 */

Map

创建Map

//定义
m := map[string]string{
    "name": "nsl",
    "sex": "nan",
}
m2 := make(map[string]int)	//m2 == emtry map
var m3 map[string]int      // m3 == nil
fmt.Println(m, m2, m3)

获取元素

Map变量名[key]

delete() 函数

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key

		/* 创建map */
        countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

        fmt.Println("原始地图")

        /* 打印地图 */
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }

        /*删除元素*/ delete(countryCapitalMap, "France")
        fmt.Println("法国条目被删除")

        fmt.Println("删除元素后地图")

        /*打印地图*/
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }

以上实例输出结果为:

原始地图
India 首都是 New delhi
France 首都是 Paris
Italy 首都是 Rome
Japan 首都是 Tokyo
法国条目被删除
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi

遍历

  • 使用range遍历key,或者遍历key,value对,

  • 遍历时,不保证遍历顺序,如需顺序,需手动对key排序

  • 使用len获取元素个数

	m := map[string]string{
		"name":    "nsl",
		"sex":     "nan",
		"country": "china",
		"age":     "20",
	}
	for k, v := range m {
		fmt.Println(k, v)
	}
	fmt.Println("---------")

	for _, v := range m {
		fmt.Println(v)
	}
	fmt.Println("---------")

	for k := range m {
		fmt.Println(k)
	}

以上实例输出结果为:

name nsl
sex nan
country china
age 20
---------
nsl
nan
china
20
---------
name
sex
country
age

Map例题

例题:寻找最长不含有重复字符的子串

abcabcbb -->abc

bbbbbb --> b

bwwkew --> wke

func lengthOfNonRepeatingSubStr(s string) int {
	lastOccurred := make(map[rune]int)
	start := 0
	maxLength := 0

	for i, ch := range []rune(s) {
		if lastI, ok := lastOccurred[ch]; ok && lastI >= start {
			start = lastI + 1
		}
		if i-start+1 > maxLength {
			maxLength = i - start + 1
		}
		lastOccurred[ch] = i
	}
	return maxLength
}

func main() {
	fmt.Println(lengthOfNonRepeatingSubStr("abcabcbb"))
	fmt.Println(lengthOfNonRepeatingSubStr("bbbbbbb"))
	fmt.Println(lengthOfNonRepeatingSubStr("pwwkew"))
	fmt.Println(lengthOfNonRepeatingSubStr(""))
	fmt.Println(lengthOfNonRepeatingSubStr("b"))
	fmt.Println(lengthOfNonRepeatingSubStr("abcdefg"))
	fmt.Println(lengthOfNonRepeatingSubStr("bbbdefg"))
	fmt.Println(lengthOfNonRepeatingSubStr("你好我是go"))
	fmt.Println(lengthOfNonRepeatingSubStr("奥利奥"))
	fmt.Println(lengthOfNonRepeatingSubStr("一二三二一"))
}

以上实例输出结果为:

3
1
3
0
1
7
5
6
2
3

补充

  • key不存在时,获得Value类型的初始值
  • 用value, ok := m[key] 来判断是否存在key

字符与字符串处理

字符类型

var ch byte // 声明字符类型
ch = 97
// 格式化输出,%c以字符型输出,%d以整型输出
fmt.Printf("ch = %c, ch = %d\n", ch, ch)

ch = 'a' // 字符以单引号
fmt.Printf("ch = %c, ch = %d\n", ch, ch)

// 字符大写转小写,小写转大写,大小写相差32,小写大
fmt.Printf("大写: %d, 小写: %d\n", 'A', 'a')
fmt.Printf("大写转小写: %c\n", 'A'+32)
fmt.Printf("小写转大写: %c\n", 'a'-32)

// 以'\'开头的代表控制字符,不在控制台打印出来
fmt.Printf("hello go %c", '\n')

fmt.Printf("hello go ")

输出结果:

ch = a, ch = 97
ch = a, ch = 97
大写: 65, 小写: 97
大写转小写: a
小写转大写: A
hello go
hello go

字符串类型

var str1 string
str1 = "acv"
fmt.Printf("str1 = %s\n", str1)
 
// 自动推导类型
str2 := "hello go world"
fmt.Printf("str2 = %s\n", str2)
fmt.Printf("str2 类型是 %T\n", str2)
 
// 内建函数,len()可以测定字符串的长度,有多少个字符
 
fmt.Printf("len(str2)的长度 %d", len(str2))

输出结果:

str1 = acv           
str2 = hello go world
str2 类型是 string   
len(str2)的长度 14  

字符和字符串的区别

var ch byte
var str string

// 字符 1.单引号 2.往往只有一个字符,转义字符除外'\n'
ch = 'a'
fmt.Printf("ch = %d\n", ch)

// 字符串 1.双引号 2.字符串由一个或者多个字符组成 3.字符串都是隐藏了一个结束符'\o'
str = "a" // 由于'a'和'\o'组成

str = "hello go"

fmt.Printf("str[0] = %c, str[1] = %c\n", str[0], str[1])

输出结果:

ch = 97               
str[0] = h, str[1] = e

结构体和方法

面向对象

  • go语言仅支持封装,不支持继承和多态
  • go语言没有class,只有struct

结构体的定义

type TreeNode struct {
	Left, Right *TreeNode
	Value int
}

结构体的创建

func createTreeNode(value int) *TreeNode {
	return &TreeNode{Value:value}
}

root.Left.Right createTreeNode(2)
  • 使用自定义工厂函数
  • 注意返回了局部变量的地址
type treeNode struct {
	value int
	left, right *treeNode
}

func createNode(value int) *treeNode {
	return &treeNode{value:value}
}

func main() {
	var root treeNode
	
	root = treeNode{value: 3}
	root.left = &treeNode{}
	root.right = &treeNode{5, nil, nil}
	root.right.left = new(treeNode)
	root.left.right = creatNode(2)
}

image-20230124142503380

为结构体定义方法

func (node TreeNode) print() {
	fmt.Print(node.Value)
}
  • 显示定义和命名方法接收者

使用指针作为方法接收者

func (node *TreeNode) setvalue(value int) {
	node.Value = value
}
  • 只有使用指针才可以改变结构体内容
  • nil指针也可以调用方法

值接收者VS指针接收者

  • 要改变内容必须使用指针接收者
  • 结构过大也考虑使用指针接收者
  • 一致性:如有指针接收者,最好都是指针接收者(建议)

包和封装

封装

  • 名字一般使用CamelCase
  • 首字母大写:public,代表全局可见,即公有方法
  • 首字母小写:private,代表包内可见,即私有方法

  • 每个目录一个包,包名可以和目录名不同名,但是建议同名
  • main包中包含可执行入口
  • 为结构定义的方法必须放在同一个包内
  • 可以是不同的文件

扩展已有类型

  • 如何扩充系统类型或别人的类型
  • 在Java中是通过继承来实现的,在go中没有继承的概念
  • go中实现扩展的方法:
    • 定义别名:最简单,但是由于以后的维护可能会改成使用组合,别名到组合不能无缝转换;需要改很多代码
    • 使用组合:最常用
    • 使用内嵌:需要省下很多代码;但是可读性较差

定义别名

//使用别名对已有类型进行扩展 用别名"queue"扩展原类型[]int,为其增加push(), pop(), isEmpty() 方法
type Queue []int

func (q *Queue) Push(v int){
    *q = append(*q, v) 
}

func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head
}

func (q *Queue) IsEmpty() bool {
    return len(*q) == 0 
}

func main() {
    q := queue.Queue{1}
    q.Push(2)
    q.Push(3)
    fmt.Printf("Pop: %d\n", q.Pop())
    fmt.Printf("Pop: %d\n", q.Pop())
    fmt.Println(q.IsEmpty())
    fmt.Printf("Pop: %d\n", q.Pop())
    fmt.Println(q.IsEmpty())    
}

以上实例输出结果为:

Pop: 1
Pop: 2
false
Pop: 3
true

使用组合

//原有类型 对树只有中序遍历

import (
    "fmt"
)

type Node struct {
    Data int
    Left, Right *Node
}

func CreatNode(data int, left, right *Node) *Node {
    return &Node{data, left, right}
}

func (node *Node) Print() {
    fmt.Print(node.Data)
}

//中序遍历
func (node *Node) InOrder() {
    if node == nil {
        return
    }
    node.Left.InOrder()
    node.Print()
    node.Right.InOrder()
}

//通过使用组合的方式对原有类型进行扩展,将原结构体"Node"组合到"MyNode"中,在MyNode中增加了后序遍历的方法
type MyNode struct {
    MyNode *tree.Node
}

//后序遍历
func (myNode *MyNode) PostOrder() {
    if myNode == nil || myNode.MyNode == nil {
        return
    }
    myLeft := MyNode{myNode.MyNode.Left}
    myLeft.PostOrder()
    myRight := MyNode{myNode.MyNode.Right}
    myRight.PostOrder()
    myNode.MyNode.Print()    
}

func main() {
    
    //创建一棵树
    root := tree.Node{0,nil,nil}
    root.Left = &tree.Node{Data:4}
    root.Right = tree.CreatNode(2, nil, nil)
    root.Left.Left = &tree.Node{3,nil,nil}
    root.Left.Right = &tree.Node{1,nil,nil}
    root.Right.Left = &tree.Node{7,nil,nil}
    
    //扩展前进行中序遍历     
    root.InOrder()          //结果:341072
    fmt.Println()
    
    //扩展后可以进行后续遍历
    myRoot := myTree.MyNode{&root}
    myRoot.PostOrder()       //结果:314720
     
}

使用内嵌

node

package tree

import "fmt"

type Node struct {
	Value       int
	Left, Right *Node
}

func CreateNode(value int) *Node {
	return &Node{Value: value}
}

func (node *Node) Print() {
	fmt.Print(node.Value, " ")
}

func (node *Node) SetValue(value int) {
	if node == nil {
		fmt.Println("node is nil")
		return
	}
	node.Value = value
}

traversesal

package tree

func (node *Node) Traverse() {
	if node == nil {
		return
	}
	node.Left.Traverse()
	node.Print()
	node.Right.Traverse()
}

entry

package main

import (
	"awesomeProject/tree"
	"fmt"
)

type myNode struct {
	*tree.Node
}

func (node *myNode) postOrder() {
	if node == nil || node.Node == nil {
		return
	}
	left := myNode{node.Left}
	left.postOrder()
	right := myNode{node.Right}
	right.postOrder()
	node.Print()
}

func (node *myNode)Traverse(){
	fmt.Println("Go to Shadowed Methods")
}

func main() {

	root := myNode{ &tree.Node{Value: 3}}
	root.Left = &tree.Node{}
	root.Right = &tree.Node{Value: 5}
	root.Right.Left = new(tree.Node)
	root.Left.Right = tree.CreateNode(2)
	root.Right.Left.SetValue(4)

	// Go to Shadowed Methods
	root.Traverse()
	// 0 2 3 4 5
	root.Node.Traverse()
	fmt.Println()


	// 2 0 4 5 3
	myRoot := myNode{ root.Node}
	myRoot.postOrder()

	// cannot use &root (type *myNode) as type tree.Node in assignment
	var baseRoot tree.Node
	baseRoot = &root
}
  • 内嵌的话就是把tree.Node的属性名给删掉了;但其实还有一个默认的属性名,就是这个struct的名字:Node

  • myNode 里面内嵌了一个tree.Node;这样的话myNode就可以使用tree.Node的所有成员变量与方法,同样的还可以使用自己的其他成员变量与方法

  • myNode可以对tree.Node的方法进行重载;不过go不叫Override,叫Shadowed;myNode调用方法时,会先检查一下自己有没有这个方法,如果有就用自己的,如果没有就用tree.Node

  • 内嵌看起来有点像继承,但是它与继承时完全不同的两个概念,继承的话可以父类引用指向子类对象;但是go语言却不支持;对编译器来说是没有审核联系的.内嵌的本质其实就是组合的语法糖

依赖管理

Go在版本1.11之前有其他的一些依赖管理工具,如:

  • govendor
  • dep
  • godep
  • golide

这些依赖管理工具查找依赖都是基于GOPATH,GOROOT或者vendor目录,并且项目代码也只能在GOPATH/src目录下面,为了解决强依赖于GOPATH的问题,Go开发了新的依赖管理工具:Go Module

Go Module解决依赖管理问题

Go module是从go-1.11推出的依赖管理项目,在v-1.11版本的时候Go mod还只是可选的,通过如下方式启用或禁用:

  • 设置环境变量:GO111MODULE = off,项目会禁用go module功能,默认还是会用其他的工具管理依赖
  • 设置环境变量:GO111MODULE = on,项目会启用go module功能,查找依赖会从当前模块的go module下查找,如果查找失败,也不会去到GOPATH或者vendor中查找依赖了
  • 设置环境变量:GO111MODULE = auto,这是默认值。这表示如果项目下存在go.mod文件并且项目不在GOPATH/src目录下,那么就会使用go module功能,否则就会禁用go module功能

注:用go module管理的项目不能在GOPATH目录下,否则就会使用其他项目中已有的包管理工具管理依赖

Go Module的使用

go init

mkdir go_pro && cd go_pro
go mod init go_pro

完成上述操作之后,就会在当前目录下生成一个go.mod文件,这个文件中的结构如下:

//module 后面是当前项目的模块名称,一般与项目名称相同,但也可以不同
//例如:如果你想将你的项目push到github上,让别人通过go get的方式引用,那么此处可以用如下:
//module github.com/YouRepoName
module go_pro

//预设的go版本
go 1.13

//用于将一个模块版本替换成另一个模块版本,前提是当前模块为主模块时,例如:
replace k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f

//也可以用于引入本地的其他模块到当前这个模块中,例如:
replace ecm-sdk-go => ./vendor/go-util

//当前项目中的依赖列表
require (
    github.com/go-git/go-git/v5 v5.1.0
)

//当前项目中禁止依赖包列表
exclude (
    github.com/go-git/go-git/v5 v5.1.0
)

go module命令

  • go mod tidy:添加丢失的依赖,删除不用的依赖
  • go mod download:下载go.mod中的依赖到本地缓存,目前所有模块版本数据均缓存在$GOPATH/pkg/mod$GOPATH/pkg/sum 下,也可以指定cache位置
  • go mod graph:显示模块依赖图
  • go mod vendor:复制依赖到vendor目录下
  • go mod verify:根据go.sum文件校验依赖
  • go mod why:解释为什么要依赖
  • go list m -json all:依赖详情
  • go list m -versions github.com/gin-gonic/gin:查看gin所有的历史版本
  • go mod edit -require="github.com/gin-gonic/gin@v1.3.0:更换依赖版本,执行完之后要执行:go mod tidy,也可以直接改go.mod文件
  • go clean -modcache:清理所有已经缓存的模块版本数据

go get命令

  • go get packageName:安装依赖
  • go get -u packageName:将依赖升级到最新的次要版本或修订版本
  • go get -u=patch packageName:将依赖升级到最新的修订版本
  • go get package@version:升级到指定的版本