这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
(内容根据字节跳动青训营课程内容以及自己的理解编写)
近期将日更这几个主题的文章,欢迎关注!
- 全面理解go协程
- channel通信
- Kitex
- Hertx
- Gorm
- go的测试环节
基本数据类型
布尔型
true或者false
比如: var b bool = true
数字类型
这里列举菜鸟教程:
字符串类型
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
多行字符串需要用:
s1 := `第一行字符串
第二行字符串
第三行字符串
`
fmt.Println(s1)
派生类型 (后面一个一个讲)
- (a) 指针类型(Pointer)
- (b) 数组类型
- (c) 结构化类型(struct)
- (d) Channel 类型
- (e) 函数类型
- (f) 切片类型
- (g) 接口类型(interface)
- (h) Map 类型
用go刷算法也是可以的,进制、科学计数法、边界值等等都是有的
a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
默认值
go的nil相当于java的null
| 类型 | 长度(字节) | 默认值 | 说明 |
|---|---|---|---|
| bool | 1 | false | |
| byte | 1 | 0 | uint8 |
| rune | 4 | 0 | Unicode Code Point, int32 |
| int, uint | 4或8 | 0 | 32 或 64 位 |
| int8, uint8 | 1 | 0 | -128 ~ 127, 0 ~ 255,byte是uint8 的别名 |
| int16, uint16 | 2 | 0 | -32768 ~ 32767, 0 ~ 65535 |
| int32, uint32 | 4 | 0 | -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名 |
| int64, uint64 | 8 | 0 | |
| float32 | 4 | 0.0 | |
| float64 | 8 | 0.0 | |
| complex64 | 8 | ||
| complex128 | 16 | ||
| uintptr | 4或8 | 以存储指针的 uint32 或 uint64 整数 | |
| array | 值类型 | ||
| struct | 值类型 | ||
| string | “” | UTF-8 字符串 | |
| slice | nil | 引用类型 | |
| map | nil | 引用类型 | |
| channel | nil | 引用类型 | |
| interface | nil | 接口 | |
| function | nil | 函数 |
byte和rune类型
相信大家对rune类型都比较陌生
byte可以当成Java里的char,一般用于记录ASCII码的一个字符 大小1字节
rune代表了一个UTF-8字符,可以表示中文韩文等等,因为UTF8编码下一个中文汉字由3~4个字节组成
举个例子:
遍历带中文的字符串:
package main
import "fmt"
func main() {
traversalString("123go语言")
}
// 遍历字符串
func traversalString(s string) {
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()
}
下面中文用的就是rune类型
这样是不是清楚多了
修改字符串 要修改字符串,需要先将其转换成
[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "hello"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1))
s2 := "博客"
runeS2 := []rune(s2)
runeS2[0] = '帅'
fmt.Println(string(runeS2))
}
值类型和引用类型
值类型和引用类型两者之间的主要区别:拷贝操作和函数传参。
值类型在使用= 赋值的时候,是拷贝复制;
值类型在函数传参的时候,也是拷贝复制
引用类型在= 和函数传参的时候,是传递的指向值的指针或者叫引用,指针和引用有细微差别。
引用类型需要通过copy函数去拷贝,否则只能用赋值
基本数据类型 int 、float、bool、string、array、结构体 struct,都是值类型;
引用类型,interface、slice、map、chan一共四种类型都是引用类型,
摘自网络:
在内存里分成两大块,栈和堆,值类型的变量通常存储在栈区,引用类型通常存储在堆区,在golang中,但有可能值类型也存储在堆区,反过来也有可能引用类型存储在栈区。
make和new的区别
我们先来看个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["测试"] = 100
fmt.Println(b)
}
这里就是声明了两个变量,肯定没啥问题吧?
有问题!
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
make 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel;
new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针。
这里摘抄地鼠文档的介绍:
make和new的区别
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
数组Array
有几点需要注意:
- 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变
- 访问越界,会panic
- 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
- 指针数组 [n]*T,数组指针 *[n]T, 如[10]int是十个int的指针组成的数组,[10]int是一个指针
数组的定义
一维数组
全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部:
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
多维数组:
全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
数组的操作
遍历数组的两种方式:
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
多维数组遍历:
var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
for k1, v1 := range f {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
}
fmt.Println()
}
数组传参:
func printArr(arr *[5]int) {
arr[0] = 10
for i, v := range arr {
fmt.Println(i, v)
}
}
func main() {
var arr1 [5]int
printArr(&arr1)
fmt.Println(arr1)
arr2 := [...]int{2, 4, 6, 8, 10}
printArr(&arr2)
fmt.Println(arr2)
}
指针
只能说当初学c、c++的时候就因为指针吃尽了苦头,选了Java没有指针很方便,现在又回来了
指针介绍
我们先来看一个简单的例子
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a )
}
一个指针变量指向了一个值的内存地址。
类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:
var var_name *var-type
举个例子:
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 )
}
空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
举个例子:
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr )
}
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
指针数组
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
指向指针的指针
指向指针的指针变量声明格式如下:
var ptr **int;
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
顺便用unsafe.Sizeof()测试一下指针的长度
指针函数传参
这里写一个swap函数举例:
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
swap(&a, &b)
fmt.Printf("交换后 a 的值 : %d\n", a)
fmt.Printf("交换后 b 的值 : %d\n", b)
}
/* 交换函数这样写更加简洁,也是 go 语言的特性,可以用下,c++ 和 c# 是不能这么干的 */
func swap(x *int, y *int) {
*x, *y = *y, *x
}
结构体
定义
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
也就相当于定义了一个类,也可以直接当函数参数
结构体指针
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* 打印 Book1 信息 */
printBook(&Book1)
}
func printBook(book *Books) {
fmt.Printf("Book title : %s\n", book.title)
fmt.Printf("Book author : %s\n", book.author)
fmt.Printf("Book subject : %s\n", book.subject)
fmt.Printf("Book book_id : %d\n", book.book_id)
}
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}
空结构体
空结构体的大小为0字节
在后面的性能优化里会有用空结构体代替一些没有用的值的地方,相当于占位符
其他还有很多
- 来道面试题
package main
import (
"fmt"
)
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "pprof.cn", age: 18},
{name: "测试", age: 23},
{name: "博客", age: 28},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
-
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
-
p3.name = “博客”其实在底层是(*p3).name = “博客”,这是Go语言帮我们实现的语法糖。
结构体相关资料
每一点都很干货
切片Slice
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(动态数组),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
(当然,有扩容就有时间损耗)
定义
你可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度。
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len)
也可以简写为
slice1 := make([]type, len)
也可以指定容量,其中 capacity 为可选参数。
make([]T, length, capacity)
这里 len 是数组的长度并且也是切片的初始长度。
初始化以及一些用法
func main() {
// 数组和切片的初始化区别
array := [3]int{1, 2}
slice := []int{1, 2}
// 根据数组定义切片
s := arr[:]
s1 := arr[start:]
s2 := arr[:end]
s := arr[startIndex:endIndex]
// len和cap函数
fmt.Print(len(slice)) // 2
fmt.Print(cap(slice)) // 2
fmt.Print(slice) // [1,2]
// 打印切片
/* 打印子切片从索引0(包含) 到索引1(不包含)*/
fmt.Println("numbers[1:4] ==", slice[0:1])
// 打印所有
fmt.Print(slice[0:])
var numbers []int
/* 允许追加空切片 */
numbers = append(numbers, 0)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
/* 同时添加多个元素 */
numbers = append(numbers, 2, 3, 4)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1, numbers)
}
range遍历
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
map
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
定义
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
/* 也可以带个长度参数*/
map_variable := make(map[key_data_type]value_data_type, len)
或者:
还可以构建一个map类型的切片:
var mapSlice = make([]map[string]string, 3)
值为切片类型的map:
var sliceMap = make(map[int][]string, 3)
方法
查找:
value, ok := map[key]
删除:
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。
遍历:
for k, v := range scoreMap {
fmt.Println(k, v)
}
接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
举例:
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
例子:
来自菜鸟教程+一些注释
package main
import (
"fmt"
)
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口 实现自定义的Error接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
// 可以直接返回错误就行
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
执行以上程序,输出结果为:
100/10 = 10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
小结
数据结构是基础,非常值得花时间去学习,尤其是转语言的同学。