Loading

Go学习笔记

环境搭建

SDK的安装和配置

go env -w GOPROXY=https://goproxy.cn,direct
  • 如果下载的是解压版的SDK需要手动配置GoROOT的环境变量,安装版不需要配置
  • (可选) Go1.14版本之后,推荐使用go mod模式来管理依赖环境,所以不需要配置GoPATH环境变量,但是下载依赖包时会默认下载到C盘不太显眼的位置,所以我这里配置到了所有Go项目的根路径,方便查找依赖包

IDE

我用jetbrains的产品用的比较多,相对来说学习成本更低,所以这里选择GoLand,当然也可以使用VsCode

Go的基本数据类型

整型

整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

类型 描述
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)

特殊整型

类型 描述
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

注意: 在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

注意事项 获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

数字字面量语法(Number literals syntax)

Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。 v := 0o377,代表八进制的 377,相当于十进制的 255。 v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。

而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 表示 v 的值等于 123456。

我们可以借助fmt函数来将一个整数以不同进制形式展示。

package main
 
import "fmt"
 
func main(){
	// 十进制
	var a int = 10
	fmt.Printf("%d \n", a)  // 10
	fmt.Printf("%b \n", a)  // 1010  占位符%b表示二进制
 
	// 八进制  以0开头
	var b int = 077
	fmt.Printf("%o \n", b)  // 77
 
	// 十六进制  以0x开头
	var c int = 0xff
	fmt.Printf("%x \n", c)  // ff
	fmt.Printf("%X \n", c)  // FF
}

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

package main
import (
        "fmt"
        "math"
)
func main() {
        fmt.Printf("%f\n", math.Pi)
        fmt.Printf("%.2f\n", math.Pi)
}

复数

complex64和complex128

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔值

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)false(假)两个值。

注意:

  1. 布尔类型变量的默认值为false
  2. Go 语言中不允许将整型强制转换为布尔型.
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

s1 := "hello"
s2 := "你好"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

转义符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠

举个例子,我们要打印一个Windows平台下的一个文件路径:

package main
import (
    "fmt"
)
func main() {
    fmt.Println("str := \"c:\\Code\\lesson1\\go.exe\"")
}

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出

字符串的常用操作

方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作
strings.TrimSpace() 去除两边的空格
strings.Trim() 去除两边的指定字符或字符串
strings.TrimLeft() 去除左边的指定字符或字符串
strings.TrimRright() 去除右边的指定字符或字符串

strconv包相关函数

stronv包提供了一系列函数用于string和基本数据数据类型的相互转换

string和int相互转换

Atoi()用于字符串转int

func Atoi(s string) (i int, err error)  //如果不能解析成string 则返回error

Itoa()用于int转string

func Itoa(i int) string
Parse系列函数

Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()。

ParseBool()
func ParseBool(str string) (value bool, err error)

返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。

ParseInt()
func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

base指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;

bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;

返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

ParseUnit()
func ParseUint(s string, base int, bitSize int) (n uint64, err error)

ParseUint类似ParseInt但不接受正负号,用于无符号整型。

ParseFloat()
func ParseFloat(s string, bitSize int) (f float64, err error)

解析一个表示浮点数的字符串并返回其值。

如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。

bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;

返回值err是*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inf,err.Error= ErrRange。

代码示例
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64)
u, err := strconv.ParseUint("2", 10, 64)

这些函数都有两个返回值,第一个返回值是转换后的值,第二个返回值为转化失败的错误信息。

Format系列函数

Format系列函数实现了将给定类型数据格式化为string类型数据的功能。

FormatBool()
func FormatBool(b bool) string

根据b的值返回”true”或”false”。

FormatInt()
func FormatInt(i int64, base int) string

返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母’a’到’z’表示大于10的数字。

FormatUint()
func FormatUint(i uint64, base int) string

是FormatInt的无符号整数版本。

FormatFloat()
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

函数将浮点数表示为字符串并返回。

bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。

fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。

prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

代码示例
s1 := strconv.FormatBool(true)
s2 := strconv.FormatFloat(3.1415, 'E', -1, 64)
s3 := strconv.FormatInt(-2, 16)
s4 := strconv.FormatUint(2, 16)

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

var a = '中'
var b = 'x'

Go 语言的字符有以下两种:

  1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
  2. rune类型,代表一个 UTF-8字符

当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

// 遍历字符串
func traversalString() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ { //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s { //rune
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}

输出:

104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) 
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河) 

因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。

字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

修改字符串

要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

func changeString() {
	s1 := "big"
	// 强制类型转换
	byteS1 := []byte(s1)
	byteS1[0] = 'p'
	fmt.Println(string(byteS1))

	s2 := "白萝卜"
	runeS2 := []rune(s2)
	runeS2[0] = '红'
	fmt.Println(string(runeS2))
}

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.

比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。

func sqrtDemo() {
	var a, b = 3, 4
	var c int
	// math.Sqrt()接收的参数是float64类型,需要强制转换
	c = int(math.Sqrt(float64(a*a + b*b)))
	fmt.Println(c)
}

运算符

Go 语言内置的运算符有:

  1. 算术运算符
  2. 关系运算符
  3. 逻辑运算符
  4. 位运算符
  5. 赋值运算符

3.1算术运算符

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

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

关系运算符

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

逻辑运算符

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

位运算符

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

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

赋值运算符

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

指针

任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

i := 56
var intPointer *int
//获取变量的地址  &变量名
intPointer = &i
fmt.Println("i的地址为", intPointer)
fmt.Println("i的地址为", &i)
//i2 := &i
var i2 *int = &i
//	指针和普通变量的区别:指针存储的是变量的内存地址,而普通类型存储的是该变量的值,那么指针也会有一个内存地址
fmt.Printf("i2的类型为%T,值为%v\n", i2, i2)
i3 := &i2
fmt.Printf("i3的类型%T", i3)
//通过*指针名获取该指针指向的地址的值
fmt.Printf("%v\n", *i2)
//可以通过这种方式修改指针指向的地址的值
*i2 = 88
fmt.Printf("%v\n", *i2)
//原来的值也发生改变
fmt.Printf("%v\n", i)

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

fmt包

fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

向外输出

标准库fmt提供了以下几种输出相关函数。

Print

Print系列函数会将内容输出到系统的标准输出,区别在于Print函数直接输出内容,Printf函数支持格式化输出字符串,Println函数会在输出内容的结尾添加一个换行符。

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

举个简单的例子:

func main() {
	fmt.Print("在终端打印该信息。")
	name := "法外狂徒 张三"
	fmt.Printf("我是:%s\n", name)
	fmt.Println("在终端打印单独一行显示")
}

执行上面的代码输出:

在终端打印该信息。我是:法外狂徒 张三
在终端打印单独一行显示

Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

举个例子:

// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
	fmt.Println("打开文件出错,err:", err)
	return
}
name := "法外狂徒 张三"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)

注意,只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串。

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

简单的示例代码如下:

s1 := fmt.Sprint("法外狂徒 张三")
name := "法外狂徒 张三"
age := 18
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("法外狂徒 张三")
fmt.Println(s1, s2, s3)

Errorf

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。

func Errorf(format string, a ...interface{}) error

通常使用这种方式来自定义错误类型,例如:

err := fmt.Errorf("这是一个错误")

Go1.13版本为fmt.Errorf函数新加了一个%w占位符用来生成一个可以包裹Error的Wrapping Error。

e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误%w", e)

格式化占位符

*printf系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

通用占位符

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

示例代码如下:

fmt.Printf("%v\n", 100)
fmt.Printf("%v\n", false)
o := struct{ name string }{"小王子"}
fmt.Printf("%v\n", o)
fmt.Printf("%#v\n", o)
fmt.Printf("%T\n", o)
fmt.Printf("100%%\n")

输出结果如下:

100
false
{小王子}
struct { name string }{name:"小王子"}
struct { name string }
100%

布尔型

占位符 说明
%t true或false

整型

占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

示例代码如下:

n := 65
fmt.Printf("%b\n", n)
fmt.Printf("%c\n", n)
fmt.Printf("%d\n", n)
fmt.Printf("%o\n", n)
fmt.Printf("%x\n", n)
fmt.Printf("%X\n", n)

输出结果如下:

1000001
A
65
101
41
41

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

示例代码如下:

f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)

输出结果如下:

6946802425218990p-49
1.234000e+01
1.234000E+01
12.340000
12.34
12.34

字符串和[]byte

占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f
%X 每个字节用两字符十六进制数表示(使用A-F)

示例代码如下:

s := "小王子"
fmt.Printf("%s\n", s)
fmt.Printf("%q\n", s)
fmt.Printf("%x\n", s)
fmt.Printf("%X\n", s)

输出结果如下:

小王子
"小王子"
e5b08fe78e8be5ad90
E5B08FE78E8BE5AD90

指针

占位符 说明
%p 表示为十六进制,并加上前导的0x

示例代码如下:

a := 10
fmt.Printf("%p\n", &a)
fmt.Printf("%#p\n", &a)

输出结果如下:

0xc000094000
c000094000

宽度标识符

宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

示例代码如下:

n := 12.34
fmt.Printf("%f\n", n)
fmt.Printf("%9f\n", n)
fmt.Printf("%.2f\n", n)
fmt.Printf("%9.2f\n", n)
fmt.Printf("%9.f\n", n)

输出结果如下:

12.340000
12.340000
12.34
    12.34
       12

其他flag

占位符 说明
’+’ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
’ ‘ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
’-’ 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
’#’ 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值;
‘0’ 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;

举个例子:

s := "小王子"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%-5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)

输出结果如下:

小王子
  小王子
小王子  
  小王子
小王子  
   小王
00小王子

获取输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

函数定签名如下:

func Scan(a ...interface{}) (n int, err error)
  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

具体代码示例如下:

func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scan(&name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端依次输入小王子28false使用空格分隔。

$ ./scan_demo 
小王子 28 false
扫描结果 name:小王子 age:28 married:false 

fmt.Scan从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数。

fmt.Scanf

函数签名如下:

func Scanf(format string, a ...interface{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

代码示例如下:

func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端按照指定的格式依次输入小王子28false

$ ./scan_demo 
1:小王子 2:28 3:false
扫描结果 name:小王子 age:28 married:false 

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。

例如,我们还是按照上个示例中以空格分隔的方式输入,fmt.Scanf就不能正确扫描到输入的数据。

$ ./scan_demo 
小王子 28 false
扫描结果 name: age:0 married:false 

fmt.Scanln

函数签名如下:

func Scanln(a ...interface{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

具体代码示例如下:

func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scanln(&name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端依次输入小王子28false使用空格分隔。

$ ./scan_demo 
小王子 28 false
扫描结果 name:小王子 age:28 married:false 

fmt.Scanln遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。示例代码如下:

func bufioDemo() {
	reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
	fmt.Print("请输入内容:")
	text, _ := reader.ReadString('\n') // 读到换行
	text = strings.TrimSpace(text)
	fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

Sscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

循环

Go中并没有whiledo-while,但是可以通过for循环实现while和do-while

for

for i := 1; i <= 10; i++ {
    fmt.Println("这是第", i, "次循环")
}

while

for {
    //条件表达式
    if i >= 10 {
        break
    }
    //	循环操作
    fmt.Printf("当前循环次数%d\n", i+1)
    //	循环变量迭代
    i++
}

do-while

for {
    //	循环操作
    fmt.Printf("当前为第%d次循环\n", i)
    //	循环条件迭代
    i++
    //	循环条件判断
    if i >= 10 {
        break
    }
}

函数

函数是组织好的、可重复使用的、用于执行指定任务的代码块。Go语言中支持函数、匿名函数和闭包。

函数定义

使用func关键字定义函数

//func 函数名(形参列表) (返回值列表){
// 执行语句
// return 返回值列表
//}
//举例
// Fibonacci 斐波那契数
func Fibonacci(n int) int {
	if n <= 2 {
		return 1
	}
	return Fibonacci(n-1) + Fibonacci(n-2)
}

注意:

  • 如果函数参数是值类型,在函数内对参数的操作不会影响原来的值,但是如果参数是引用类型,对形参的操作则直接影响原来实际参数的值

    • 值类型:基本数据类型,int,float,bool,string,以及数组和struct
      特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放
    • 引用类型:指针,slice,map,chan等都是引用类型
      特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
  • Go中的函数不支持重载

  • 在Go中,函数也是一种数据类型,因此函数的形参也可以是函数,例如下面这种写法

    //param1是函数变量
    func myFunc1(param1 func(int, int) int, param2 int, param3 int) int {
    	return param1(param2, param3)
    }
    
  • Go支持使用Type关键字自定义数据类型,所以上面的写法也可以简化成下面的写法

    type myType func(int, int) int
    
    func myFunc2(param1 myType, param2 int, param3 int) int {
    	return param1(param2, param3)
    }
    
  • Go中的函数可以有多个返回值,且可以为返回值命名,这样可以在函数中直接操作返回值,不需要额外声明变量,但是return关键字是必须的,不可省略

    func sum(param1 int, param2 int) (sum int,dif int) {
    	sum = param1 + param2
    	dif = param - param2
    	return
    }
    
  • Go的函数支持可变参数,和Java一样,Go的可变参数必须在形参列表的最后一位

    func intSum2(x ...int) int {
        ////可变参数在函数体中实际上是一个切片
    	fmt.Println(x) 
    	sum := 0
    	for _, v := range x {
    		sum = sum + v
    	}
    	return sum
    }
    

匿名函数

定义方式

//在定义时直接调用
func(param1 int, param2 int) int {
    return param2 + param1
}(1, 2)
//将匿名函数通过一个变量接收,日后调用
anonymousFunc := func(param1 int, param2 int) int {
    return param2 + param1
}
anonymousFunc(1, 2)

闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

使用闭包实现累加器

func closure() func(int) int {
	var count int = 0
	return func(i int) int {
		count += i
		return count
	}
}

练习

判断字符串是否有指定后缀,如果有则直接返回,没有的话就拼接上指定后缀
使用闭包的好处是我们不需要每次传入后缀,判断重复后缀的后缀,我们只需要传一次

func makeSuffix(suffix string) func(string) string {
   return func(s string) string {
      if strings.HasSuffix(s, suffix) {
         return s
      } else {
         return s + suffix
      }
   }
}

内置函数

Go还提供了一系列系统内置函数,不需要导包即可使用,可以在builtin\butiltin.go文件中找到他们

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

defer关键字

看一段代码

func _defer() {
	i := 1
	defer fmt.Println("defer 1,i = ", i)
	defer fmt.Println("defer 2,i = ", i)
	i++
	fmt.Println("after defer, i = ", i)
}

可以看到该函数里有两行代码是在是再defer关键字后面,当函数执行到时,暂时不会执行,会将defer的语句压入到一个栈中 defer 涉及到的变量也是拷贝到栈中,但是此后对变量的操作不会影响拷到defer栈后的值, 等到该函数执行完毕后 按照先进后出的顺序执行defer语句

time包—日期时间处理

时间类型

Go中使用time包下的Time结构体表示时间

//获取当前时间
now := time.Now()
//分别打印年月日时分秒
fmt.Printf("now type %T, now value %v\n", now, now)
fmt.Printf("年 = %v\n", now.Year())
fmt.Printf("月 = %v\n", int(now.Month()))
fmt.Printf("日 = %v\n", now.Day())
fmt.Printf("时 = %v\n", now.Hour())
fmt.Printf("分 = %v\n", now.Minute())
fmt.Printf("秒 = %v\n", now.Second())

Time结构体

Time结构体中绑定了一系列对于时间的运算函数

Add

Go语言的时间对象有提供Add方法如下:

func (t Time) Add(d Duration) Time

举个例子,求一个小时之后的时间:

func main() {
	now := time.Now()
	later := now.Add(time.Hour) // 当前时间加1小时后的时间
	fmt.Println(later)
}

Sub

求两个时间之间的差值:

func (t Time) Sub(u Time) Duration

返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。

Equal

func (t Time) Equal(u Time) bool

判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。

Before

func (t Time) Before(u Time) bool

如果t代表的时间点在u之前,返回真;否则返回假。

After

func (t Time) After(u Time) bool

如果t代表的时间点在u之后,返回真;否则返回假。

Unix Time

Unix Time是自1970年1月1日 00:00:00 UTC 至当前时间经过的总秒数。下面的代码片段演示了如何基于时间对象获取到Unix 时间。

// timestampDemo 时间戳
func timestampDemo() {
	now := time.Now()        // 获取当前时间
	timestamp := now.Unix()  // 秒级时间戳
	milli := now.UnixMilli() // 毫秒时间戳 Go1.17+
	micro := now.UnixMicro() // 微秒时间戳 Go1.17+
	nano := now.UnixNano()   // 纳秒时间戳
	fmt.Println(timestamp, milli, micro, nano)
}

time 包还提供了一系列将 int64 类型的时间戳转换为时间对象的方法。

// timestamp2Time 将时间戳转为时间对象
func timestamp2Time() {
	// 获取北京时间所在的东八区时区对象
	secondsEastOfUTC := int((8 * time.Hour).Seconds())
	beijing := time.FixedZone("Beijing Time", secondsEastOfUTC)

	// 北京时间 2022-02-22 22:22:22.000000022 +0800 CST
	t := time.Date(2022, 02, 22, 22, 22, 22, 22, beijing)

	var (
		sec  = t.Unix()
		msec = t.UnixMilli()
		usec = t.UnixMicro()
	)

	// 将秒级时间戳转为时间对象(第二个参数为不足1秒的纳秒数)
	timeObj := time.Unix(sec, 22)
	fmt.Println(timeObj)           // 2022-02-22 22:22:22.000000022 +0800 CST
	timeObj = time.UnixMilli(msec) // 毫秒级时间戳转为时间对象
	fmt.Println(timeObj)           // 2022-02-22 22:22:22 +0800 CST
	timeObj = time.UnixMicro(usec) // 微秒级时间戳转为时间对象
	fmt.Println(timeObj)           // 2022-02-22 22:22:22 +0800 CST
}

时间格式化

time.Format函数能够将一个时间对象格式化输出为指定布局的文本表示形式,需要注意的是 Go 语言中时间格式化的布局不是常见的Y-m-d H:M:S,而是使用 2006-01-02 15:04:05.000(记忆口诀为2006 1 2 3 4 5)。

其中:

  • 2006:年(Y)
  • 01:月(m)
  • 02:日(d)
  • 15:时(H)
  • 04:分(M)
  • 05:秒(S)

实例代码

currentTime := time.Now()

fmt.Println("当前时间  : ", currentTime)

fmt.Println("当前时间字符串: ", currentTime.String())

fmt.Println("MM-DD-YYYY : ", currentTime.Format("01-02-2006"))

fmt.Println("YYYY-MM-DD : ", currentTime.Format("2006-01-02"))

fmt.Println("YYYY.MM.DD : ", currentTime.Format("2006.01.02 15:04:05"))

fmt.Println("YYYY#MM#DD {Special Character} : ", currentTime.Format("2006#01#02"))

fmt.Println("YYYY-MM-DD hh:mm:ss : ", currentTime.Format("2006-01-02 15:04:05"))

fmt.Println("Time with MicroSeconds: ", currentTime.Format("2006-01-02 15:04:05.000000"))

fmt.Println("Time with NanoSeconds: ", currentTime.Format("2006-01-02 15:04:05.000000000"))

fmt.Println("ShortNum Month : ", currentTime.Format("2006-1-02"))

fmt.Println("LongMonth : ", currentTime.Format("2006-January-02"))

fmt.Println("ShortMonth : ", currentTime.Format("2006-Jan-02"))

fmt.Println("ShortYear : ", currentTime.Format("06-Jan-02"))

fmt.Println("LongWeekDay : ", currentTime.Format("2006-01-02 15:04:05 Monday"))

fmt.Println("ShortWeek Day : ", currentTime.Format("2006-01-02 Mon"))

fmt.Println("ShortDay : ", currentTime.Format("Mon 2006-01-2"))

fmt.Println("Short Hour Minute Second: ", currentTime.Format("2006-01-02 3:4:5"))

fmt.Println("Short Hour Minute Second: ", currentTime.Format("2006-01-02 3:4:5 PM"))

fmt.Println("Short Hour Minute Second: ", currentTime.Format("2006-01-02 3:4:5 pm"))

定时器

使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

func tickDemo() {
	ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
	for i := range ticker {
		fmt.Println(i)//每秒都会执行的任务
	}
}

异常处理

个人感觉Go的异常处理机制比较简陋,主要通过两个内置函数进行异常的处理

  • panic(v any):个人感觉有点类似于Java的Thorw关键字,抛出一个异常,结束该代码块的执行,它可以在代码任何地方调用
  • recovery():类似于Java的Catch,但是需要在deferdefer代码块中调用

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		//如果程序出出现了panic错误,可以通过recover恢复过来
		if err != nil {
			fmt.Println("recover in B")
		}
	}()
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

数组

数组的定义

一维数组

//数组定义 定义一个字符串类型长度为2的数组
var persons [2]string
persons[0] = "zhangsan"
persons[0] = "lisi"

//定义数组并初始化
var foods = [2]string{"apple", "meat"}

//长度不确定时,可以用...代替 编辑器会推断数组长度
var students = [...]string{"xiaolan", "xianghong", "zhangsan"}

//使用下标对数组初始化
var hobbies = [4]string{0: "唱", 2: "跳", 3: "rap"}

二维数组

//二维数组 ,只有外层的长都可以让编译器推断长度 内层不行
var citys = [...][2]string{
	{"北京", "上海"},
	{"深圳", "广州"}}

如果想在数组中放入不同的数据类型的元素,可以将数组定义为any 类型,但是一般不建议这么做,因为很这样对数据的操作可能还要使用类型断言

//any是Go1.18新引入的关键字,在这之前使用的是interface{}
var array [10]any
//或者
var array [10]interface{}

遍历数组

通过for循环遍历

var hobbies = [4]string{0: "唱", 2: "跳", 3: "rap"}
for i:=0;i<len(hobbies);i++{
    fmt.Println(hobbies[i])
}

通过for-range遍历

var students = [...]string{"xiaolan", "xianghong", "zhangsan"}
//第一个参数为当前遍历的下标,如果不关心当前下标,可以用"_"代替
//第二个参数为当前遍历的数组元素
//参数名可以自己定义
for index, student := range students {
    fmt.Println(student)
}

注意:Go数据中元素的地址是连续的,具体间隔多少由存放的数据类型决定,比如一个数组是int32类型的,则各个元素的地址间隔为4,因为int32占用4个字节,但是由于在Go中不能对指针进行运算,所以个人感觉这个特性对程序员来说意义不是很大。

切片(slice)

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片的底层主要有三个部分组成:

  • 底层数组
  • 长度
  • 容量

切片定义

基本语法

var name []T

其中,

  • name:表示变量名
  • T:表示切片中的元素类型
  1. 让切片直接引用创建好的数组
var intArr [5]int = [...]int{1, 2, 3, 4, 5}
	//定义切片
	//方式一 让切片取引用创建好的数组
	var intSlice []int
	//将数组元素放到切片中,左闭右开区间
	intSlice = intArr[0:3]
	//简便写法
	//数组所有元素
	intSlice = intArr[:]
	//第三个开始后面的所有元素
	intSlice = intArr[2:]
	//0到第三个元素
	intSlice = intArr[:3]

  1. 使用内置函数make(),使用这种方式定义时,第二个参数的值必须小于等于第三个参数,第二个参数为创建切片的长度,第三个参数为创建切片的容量
//	方式二 使用内置函数make 创建一个长度为零容量为10的整形切片
	ints := make([]int, 0, 10)
  1. 定义切片时直接赋值
//	方式三 定义时直接赋值
strings := []string{"tom", "jerry"}
fmt.Println(strings)

切片的遍历

遍历切片和遍历数组是一样的,这里不再赘述

切片的增加删改查

使用内置函数append()向切片的中插入元素,注意这里一定要有变量来接受,否则编译无法通过,一般就是通过原始切片变量来接收

ints := make([]int, 0, 10)
ints = append(ints,100)

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

从开头位置删除

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}a = a[1:] // 删除开头1个元素a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}a = append(a[:0], a[1:]...) // 删除开头1个元素a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

a = []int{1, 2, 3}a = a[:copy(a, a[1:])] // 删除开头1个元素a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除
a = []int{1, 2, 3}a = a[:len(a)-1] // 删除尾部1个元素a = a[:len(a)-N] // 删除尾部N个元素

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况

直接通过变量名加索引修改指定下标的值

a = []int{1, 2, 3}
a[0] = 100

直接通过变量名加索引获取指定下标的值

切片的扩容机制

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

注意事项

切片作为形参时,实际上是值传递,并非引用传递

看一段代码

func main() {
	var arr = []int{1, 2, 3, 4, 5}
	fmt.Printf("arr pointer: %p\n", &arr)
	test(arr)
	fmt.Printf("arr: %v\n", arr)
}
 
 
func test(data []int) {
	fmt.Printf("data pointer: %p\n", &data)
	data[0] = 100
}

输出结果

arr pointer: 0xc000004078
data pointer: 0xc000004090
arr: [100 2 3 4 5]

可以看到,输出的地址值确实是不相同的,这也就证明了,在进行函数调用时,确实是将实参的值拷贝到一个新的地址,但是我们发现在函数中对修改形参切片的值时,确实也影响到了实参切片的值,这是因为切片底层维护的实际上是一个数组指针,通过函数传参后这个指针的值并没有改变,因此在修改切片的值时,实际上是操作这个指针指向的数组,这样的话就会影响到实参的值

切片的运行时结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

另外一段代码

func main() {
	var arr = []int{1, 2, 3, 4, 5}
	fmt.Printf("arr pointer: %p\n", &arr)
	test(arr)
	fmt.Printf("arr: %v\n", arr)
}
 
 
func test(data []int) {
	fmt.Printf("data pointer: %p\n", &data)
    data = append(data, 100)
	data[0] = 100
}

输出结果

arr pointer: 0xc000004078
data pointer: 0xc000004090
arr: [1 2 3 4 5]   

这是因为通过append函数扩充一个元素时,由于原切片的容量不足,导致底层数组需要扩容,而扩容后的底层数组的地址改变了,因此函数中的data的结构体中的array值改变了,而后的data[0] = 100语句操作的已经是新的底层数组了,因此也就与函数外的原切片中指向的底层数组不是同一个了。

总结:如果明确要修改实参切片的值,那么函数参数就应该使用指针

附属小问题:

var arr = []int{1, 2, 3, 4, 5}
fmt.Printf("arr pointer: %p\n", arr)
fmt.Printf("arr pointer: %p\n", &arr)

上面👆🏻代码中两行输 出语句的区别是什么?

答案:对于切片来说,使用%p格式化输出时,如果前面不加取地址符,那么打印的是切片中第一个元素的地址;如果前面加上取地址符&,那么打印的是该切片的地址。

映射(map)

map的定义

map[key的类型]value的类型

  • make()内置函数

    m := make(map[string]string, 10)
    name := "name"
    m[name] = "zhangsan"
    fmt.Println(m)
    
  • 定义时直接赋值

    m1 := map[string]string{
        name: "zhangsan",
    }
    //也可以像这样
    m1 := map[string]string{}
    m1[name] = "zhansan"
    

    但是如果通过var关键字定义时,就必须使用make为map分配地址,否则会报错:panic: assignment to entry in nil map 这就类似于Java的空指针

    var m2 map[string]string
    m2 = make(map[string]string)
    m2[name] = "zhangsan"
    

map的遍历

使用for-range遍历map,k为键,v为值

func iter() {
	m := make(map[string]interface{})
	m["name"] = "zhangsan"
	m["age"] = 19
	m["addr"] = "北京八宝山"
	for k, v := range m {
		fmt.Printf("%v=%v\n", k, v)
	}
}

delete()

使用Go提供的内置函数来删除map的中的元素

m1 := map[string]string{
    name: "zhangsan",
}
delete(m1,"name")

结构体(struct)

结构体的这个特性类似于Java的Class,它也有属性,同时也可以为结构体绑定方法,所以在Go中是依靠结构体来实现面向对象这个特性

结构体定义

//首字母大写表示可以被其他包访问
type Cat struct {
	//同样 属性的首字母大写 才能被其他包访问
	//结构体的属性
	Name  string `json:"name"`       //序列化成json的字段名称
	Age   int    `json:"age,string"` //序列化成字符串
	Color string `json:"color"`
}

☝️向上面定义的那样,我们可以使用反引号为结构体属性起别名,用于指定将其序列化后的字段名称,同时可以看到在Age属性后面我们指定定了将其序列化成string类型,这是通过反射的的特性实现的。

注意:如果某个属性需要序列化,则改属性的首字母必须大写,因为我们序列化成json串也是调用系统提供的函数,因此要保证该属性必须在其他包可以访问

定义结构体变量

直接使用代码示例更加清晰:

//创建结构体变量的方式
//方式一
cat1 := Cat{Name: "小白", Age: 1, Color: "白色"}
//方式二 定义
var cat2 Cat
cat2.Name = "小黄"
cat2.Age = 1
cat2.Color = "黄色"
//方式三 指针 注意 :这里返回的实际是一个cat类型的指针
cat3 := new(Cat)
//严格来说应该用(*cat3).Name = "小黑"使用,但是Golang允许直接使用cat3.Name = "小黑"
(*cat3).Color = "黑色"
(*cat3).Name = "小黑"
(*cat3).Age = 1
//方式四 指针的第二种写法, 同样支持方式三的简便写法,可以直接赋值
cat4 := &Cat{Name: "小橘", Age: 1, Color: "橘黄色"}

注意上面的第三和第四种方式,返回的其实是结构体类型的指针,我们对其操作时,标准的写法应该是(*cat3).Color = "黑色",但是这种方式并不是特别简洁,因此Go支持这样的写法cat3.Name = "小黑"

结构体方法

还是上文的Cat结构体,我们可以为其绑定方法,定义方式和函数基本相同

//将方法绑定给结构体
func (receiver Cat) Run1() {
	fmt.Println("I am running")
}

//将方法绑定给结构体指针
func (receiver *Cat) Run2() {
	fmt.Println("I am running")
}
  • 和Java一样,调用结构体的方法也是通过变量名.方法名()

  • 方法也可以有返回值,只不过示例中没写

  • 注意上面的receiver,这个变量名可以自己指定,其用途和Java中的this指针类似

  • 对于run1(),直接通过变量名.方法名()调用,对于run2(),标准写法应该是指针变量名.方法名(),但是也支持前者的写法,注意这两种方式是有本质区别的,如果在方法中对结构体变量进行某种操作,run1()这种定义方式不会影响实参,但是run2()是直接操作实参。简而言之run1()是值传递,run2是引用传递,传递的是变量地址的拷贝

面向对象

Go面向对象的特性主要是通过结构体和接口实现的,Go并没有extendsimplement关键字

封装

Java中封装的特性是用类来体现的,但是在Go中封装的特性是通过包来实现,在同一个包中,方法和变量可以共享,但是如果没有在同一个包中,要想在其他包使用,函数名和方法名以及变量名的首字母必须大写,类似于Java的Public。首字母小写的则和Java的Private相似。

继承

上面已经提到,Go没有Extends关键字。Go中的继承是依靠函数体的匿名字段实现。

//商品
type Goods struct {
   Name  string
   price float32
}
//书籍
type Books struct {
	Goods  //匿名字段
	author string
}
//Goods的方法
func (receiver Goods) GetPrice() float32 {
	fmt.Println("Goods ")
	return receiver.price
}
//测试方法  可以看到,book变量可以直接使用Goods的方法和字段
func test01() {
	book := &Books{Goods{Name: "霍乱时期的爱情", price: 99}, "马尔克斯"}
	fmt.Println(book.price)
	fmt.Println(book.GetPrice())

}

多态

Go的多态是通过接口来体现的,先看示例代码

//定义一个Usb接口
type Usb interface {
   transformData()
}
//分别定义Phone结构体和Camera结构体,并为其绑定一个transformData()方法
type Phone struct {
}

func (p *Phone) transformData() {
	fmt.Println("手机正在传输数据")
}
func (p *Phone) Call() {
	fmt.Println("手机正在打电话")
}

type Camera struct {
}

func (c *Camera) transformData() {
	fmt.Println("相机正在传输数据")
}

func main(){
	var usb1 Usb = new(Phone)
	var usb2 Usb = new(Camera)
	usb1.transformData()
    usb2.transFormData()
}

在上面的示例代码,我们定义了两个结构体,同时为他们绑定了方法,而这个方法正是Usb接口中定义的方法,那么这两个接口就实现了Usb接口,因此在测试中,可以直接使用Usb类型来接收两个结构体实例,并调用各自实现的方法。

注意:

  • 要想实现一个接口,必须实现该接口中定义的所有方法,缺一不可

  • 我们可以在方法和函数的形参列表中使用接口类型,使两者更加通用

  • 在实例代码中,Phone结构体还有一个自己的方法——Call(),但是在由于在测试中我们使用的是Usb类型来接收该变量,是无法直接调用的,这时我们可以是用类型断言将其转换成Phone实例,进而实现调用

    if phone, success := usb1.(*Phone); success {
            phone.Call()
    }
    
  • Go的接口不能提供默认实现

Io

本节只涉及文件的基本使用

文件操作

文件的相关方法在os包中,大致有以下方法,列举几个常用的

方法 描述
Open(name string) (*File, error) 以只读模式打开文件
OpenFile(name string, flag int, perm FileMode) (*File, error) 按照指定模式打开文件
Create(name string) (*File, error) 新建文件并打开
Rename(oldpath, newpath string) 文件重命名
ReadFile(name string) ([]byte, error) 直接读取文件内容
WriteFile(name string, data []byte, perm FileMode) 直接写入文件

文件读取

除了使用os.ReadFile(name string)的方法之外,还可以使用File结构体提供的方法进行文件读取。

file, err := os.Open("hello.txt")
defer file.Close()
if err == io.EOF {
   fmt.Println(err)
}
bytes := make([]byte, 10)
for true {
   n, err := file.Read(bytes)
   if err != nil {
      break
   }
   fmt.Printf("读取到%d字节的数据:%s\n", n, string(bytes))
}

另外还可以使用bufio包的Reader结构体提供的方法,该结构体还提供了很多方法,这里不再一一赘述

reader := bufio.NewReader(file)
for {
   content, err := reader.ReadString('\n') //一次读取部分信息,以换行符为标识 表示一次读取一行
   if err == io.EOF {
      fmt.Println("文件读取结束")
      break
   }
   fmt.Println(content)

}

文件写入

和文件读取类似,我们也可以使用File或者Writer提供的方法进行文件的写入

//创建一个hello.txt文件  等效于 OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
file, err := os.Create("E:/hello.txt")
defer file.Close()
if err != nil {
   log.Fatal(err)
}
//直接写入
_, err = file.Write([]byte("hello\n"))
//带缓冲区的方式写入
writer := bufio.NewWriter(file)
writer.Write([]byte("hello\n"))
if err != nil {
   log.Fatal(err)
}
//因为writer是带缓存的 所以在调用write方法时  实际上是写入到缓存中,
//所以要调用flush将缓存刷新到文件
writer.Flush()

Json

Go内置的Json的序列化和反序列化工具,使用非常简单方便

json.Marshal()序列化成json

json.Unmarshal()json反序列化

实例:

func mapToJson() {
   m := make(map[string]any, 0)
   m["name"] = "zhangsan"
   m["age"] = 22
   m["hobby"] = "唱 跳 rap"
   bytes, err := json.Marshal(m)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(string(bytes))
}

//注意 要想被序列化 结构体的字段必须以大写字母开头,否则在json包中无法访问,就不会序列化对应的字段
//使用tag标签可以指定序列化后json串的key 可以指定多种转换格式tag
type Person struct {
   Name  string `json:"name" xml:"name"`
   Age   int    `json:"age,string" xml:"age"` //序列化为字符串
   Hobby string `json:"hobby" xml:"hobby"`
}

func structToJson() {
   person := Person{"zhangsan", 21, "唱跳 rap"}
   bytes, err := json.Marshal(&person)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(string(bytes))
}
func sliceToJson() {
   var persons []Person
   persons = append(persons, Person{"张三", 12, "看罗老师刑法小课堂"})
   persons = append(persons, Person{"于谦", 18, "抽烟喝酒烫头"})
   persons = append(persons, Person{"罗翔", 30, "给张三讲刑法"})
   marshal, err := json.Marshal(persons)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(string(marshal))
}
func jsonToSlice() {
   s := "[{\"name\":\"张三\",\"age\":\"12\",\"hobby\":\"看罗老师刑法小课堂\"},{\"name\":\"于谦\",\"age\":\"18\",\"hobby\":\"抽烟喝酒烫头\"},{\"name\":\"罗翔\",\"age\":\"30\",\"hobby\":\"给张三讲刑法\"}]\n"
   m := make([]Person, 0, 10)
   err := json.Unmarshal([]byte(s), &m)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println(m)
}

posted @ 2022-06-23 14:22  _fun_ny  阅读(97)  评论(0)    收藏  举报