一个GO代码,hello,world
说到学习语言,那必然少不了我们的hello world了,先来看个简单的hello world代码
go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
- 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- 下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
是不是感觉有点 java + c
的味道?笑
学过 java
的应该都知道println
是输出并换行,这里好像无非就是把system.out
换成了fmt
,并且需要导入fmt
这个包
然后又综合了python
,句末不需要写分号, 并且if else
, for
后不需要加括号
但是这里需要注意的是,他这个括号还就必须这样打了,不能单独打一行
例如:
接下来是go的一些基础语法
Go标识符
Go的标识符和c一样,是由字母、数字、下划线组成,并且第一个字符必须是字母或下划线,而不能是数字。
GO关键字
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 |
println | real | recover | string | true | uint | uint8 | uintptr |
GO运算符
与c++基本一致,需要注意的就是go的++和--不能参与运算,所以++和--也是只能放在变量的后面
GO语言注释
和c++一样, //
注释单行, /* .... */
注释多行
GO的变量与常量声明
变量:var + name + type = value
或者 name := value
使用 :=
的话go就会自动推导类型,所以两种方式实际上是一样的,所以要注意前者不能和后者混用
注意 : := 符号只允许在函数中使用,即只能在声明局部变量的时候使用,而 var 没有这个限制。
例如:
go
package main
import "fmt"
func main() {
var a int = 3
fmt.Println(a)
b := 4
fmt.Println(b)
}
输出结果为:
go
3
4
下面这样就是错误的
下面这样也是错误的
另外,还可以一次声明多个变量并且赋值
go
package main
import "fmt"
func main() {
var a, b, c int = 3, 4 ,5
fmt.Println(a, b, c)
}
常量:const + name + type = value
例如:
go
package main
import "fmt"
func main() {
const a int = 3
fmt.Println(a)
}
常量之后不能进行值的改变,不然就会报错
同样也可以一次声明多个值
go
package main
import "fmt"
func main() {
const a, b, c int = 3, 4, 5
fmt.Println(a, b, c)
}
GO语言基本语句
if else
与c++类似,也支持嵌套。不同的是if后面的表达式不需要写括号,并且if后面一定要跟大括号,建议根据以下格式规范写。
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
例如:
go
func main() {
a := 100
if a >= 100 {
fmt.Println(">=100")
} else {
fmt.Println("<100")
}
}
switch语句
也与c++类似,但是这里的case默认自带break,也就是在成功执行一个case之后就会跳出,c++是遇到一个符合条件的case之后会一直执行后面case的语句直到遇到break才会停止。
go
switch var1 {
case value1:
...
case value2:
...
default:
...
}
例如:
go
package main
import "fmt"
func main() {
score := 'A'
switch score {
case 'A':
fmt.Println("太棒了")
case 'B':
fmt.Println("继续加油")
case 'C':
fmt.Println("要努力哦")
default:
fmt.Println("下次加油哦")
}
}
结果为:
go
太棒了
fallthrough
想要实现c++的效果,可以使用fallthrough ,其实c++也有这玩意儿就是了
使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
例如:
go
package main
import "fmt"
func main() {
score := 'A'
switch score {
case 'A':
fmt.Println("太棒了")
fallthrough
case 'B':
fmt.Println("继续加油")
fallthrough
case 'C':
fmt.Println("要努力哦")
fallthrough
default:
fmt.Println("下次加油哦")
}
}
结果为:
go
太棒了
继续加油
要努力哦
下次加油哦
for循环
go语言中没有while,但是能用for循环实现while的效果,基本用法也与c++相似。
下面是一个简单的for循环:
go
for i := 1; i <= 5; i++ {
fmt.Println(i)
}
可以通过以下方式实现while的效果:
go
func main() {
n := 5
for n > 0 {
n--
fmt.Println(n)
}
}
可以这样实现无限循环:
go
for {
n--
fmt.Println(n)
}
要停止无限循环,可以在命令窗口按下ctrl-c
For-each range
这种格式的循环可以对字符串、数组、切片、map等进行迭代输出元素。
这里要注意,遍历的时候,字符串、数组、切片这些有序的要用两个值 ,第一个是下标,第二个才是value。无序的map也要用两个值,一个是key,一个是value。同样也是不需要的值不使用就行了, 不想用的值可以使用_
代替
例如:
go
package main
import "fmt"
func main() {
s := "may all the beauty blessed"
strs := [3]string{"A", "B", "C"}
slice := []int{1, 2, 3}
map1 := map[int]string{
0: "48",
1: "49",
2: "50",
}
for i, c := range s {
fmt.Print(i, string(c))
fmt.Print(" ")
}
fmt.Println()
for _, s := range strs {
fmt.Print(s)
fmt.Print(" ")
}
fmt.Println()
for i, num := range slice {
fmt.Print(i, num)
fmt.Print(" ")
}
fmt.Println()
for key, value := range map1 {
fmt.Print(key, value)
fmt.Print(" ")
}
}
输出结果为:
go
0m 1a 2y 3 4a 5l 6l 7 8t 9h 10e 11 12b 13e 14a 15u 16t 17y 18 19b 20l 21e 22s 23s 24e 25d
A B C
0 1 1 2 2 3
048 149 250
// 前面的0,1, 2...是下标啊
这里还发现了print,类型相同会分开,类型不同不会分开,qwq
GO语言数据类型
Go是一种强类型语言
总共有四大类
- 基础类型
- 聚合类型
- 引用类型
- 接口类型
基础类型
基础类型与c语言类似,
有分为int, uint, float, complex, string, bool
int:
uint:
uintptr与unsafe.Pointer
这个uintptr是啥呢?这个牵扯到了指针,详情看深度解密Go语言之unsafe - 知乎 (zhihu.com)
按我的理解就是:
- go语言中是有c语言中的指针的机制的,但是他为了安全,有种种限制,go语言中的一般指针是不能进行数学运算的。
下面用具体样例进行解释:
先来看个简单的指针用法,可以完全类比c
go
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
p := &a[0]
c := 3
c++
fmt.Println(*p)
fmt.Println(c)
}
输出结果为:
1
4
这里熟悉c指针的伙伴们应该都没问题,但是go的指针他不能进行运算,例如
可以发现这里报错了
那么我们有没有办法实现c语言中,也是我们想要的这种指针p++
取到a[1]
的值的效果呢?
那就得用到uintptr
了
go
package main
import (
"fmt"
"unsafe"
)
func main() {
a := [5]int{1, 2, 3, 4, 5}
d := &a[0]
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(d)) + 8)))
}
这里涉及到了两类指针,和我们的uintptr类型
- *类型:普通指针类型,用于传递对象地址,不能进行指针运算。
- unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。
- uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。
unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。
我们这里的操作是先把d指针从*int
类型转化成了unsafe.Pointer
类型,但是unsafe.Pointer
类型不能进行运算,我们再转化为uintptr
进行运行,我这里是人为计算出了一个int值占8个字节(go语言,一个int我是64位电脑就是8个字节,刚开始以为4个算错懵逼了一会儿,哭😥),当然我们也可以使用unsafe.Sizeof
算字节,改成以下即可:
go
func main() {
a := [5]int{1, 2, 3, 4, 5}
d := &a[0]
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(d)) + unsafe.Sizeof(a[0]))))
}
计算完毕之后再重新转化为unsafe.Pointer
类型,然后再转为为*int
类型, 再*
运算取结果,这样我们就利用指针++取到了a[1]
的值,当然这里是为了便于理解,用了如此丑陋的方式...
float:
这里需要注意的是浮点型没有float,而一定要选择float32或者float64
complex:
这里也要注意必须指定是complex64还是complex128
complex64 对应的是float32,是所有实部为float32,虚部为float32的集合
complex128 对应的是float64,是所有实部为float64,虚部为float64的集合
string:
与c++类似,可以直接相加运算
go
package main
import "fmt"
func main() {
var s1 string = "qwq"
var s2 string = "qaq"
var s3 string = s1 + s2
fmt.Println(s3)
}
输出结果为:
go
qwqqaq
bool:
go
var flag bool = false
真为true
假为false
默认值
go 语言可以不对变量进行初始化,所有数据类型都有默认值。因此我们可以不用担心变量未初始化问题。
int
类型和uint
类型的0
float32
和float64
类型的0.0
bool
类型的false
string
类型的空字符串
complex
类型的(0+0i)
rune类型
这里有个问题,就是go的基础类型中并没有char类型。但是go中有rune类型,这是int32的别称,可以当做是c中的char类型,使用了utf-8编码,数字和字母的编码和我们常见的ascii编码值是一样的。
go
package main
import "fmt"
func main() {
c := 'A'
c1 := 'a'
c2 := '0'
var c3 rune
c3 = '1'
fmt.Println(c, c1, c2, c3)
}
输出结果为:
go
65 97 48 49
聚合类型
Go的聚合类型分为数组
和结构体
数组
go中的数组也与c++基本一致
数组的定义:var arrayName [size]dataType
其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中元素的数据类型。
例如,以下定义了name为arraym, size为10, type为int的数组
var array [10]int
数组初始化
数组元素如果不进行初始化的话,默认为初始化为0
下面是两种初始化方法:
go
package main
import "fmt"
func main() {
// 第一种
var array1 = [5]int{1, 2, 3, 4, 5}
// 第二种
array2 := [5]int{1, 2, 3, 4, 5}
fmt.Println(array1[1], array2[1])
}
如果数组长度不确定,可以使用[...]
代替数组的长度,编译器会根据元素个数自行推断数组的长度:
go
package main
import "fmt"
func main() {
// 第一种
var array1 = [...]int{1, 2, 3, 4, 5}
// 第二种
array2 := [...]int{1, 2, 3, 4, 5}
fmt.Println(array1[1], array2[1])
}
如果设置了数组的长度,我们还可以通过 指定下标 来初始化元素, 没有指定的会默认初始化为0:
go
package main
import "fmt"
func main() {
// 第一种
var array1 = [5]int{0: 1, 1: 2}
// 第二种
array2 := [5]int{0: 1, 1: 2}
fmt.Println(array1[1], array2[4])
}
数组的应用
与c++一致,可以按下标访问,修改元素
go
package main
import "fmt"
func main() {
a1 := [5]int{1, 2, 3, 4, 5}
fmt.Println(a1[1])
a1[1] = 0
fmt.Println(a1[1])
}
输出结果为:
go
2
0
数组的遍历:
可以类似c++用下标遍历,也可以使用for-each遍历
go
package main
import "fmt"
func main() {
strs := [3]string{"qwq", "qaq", "=.="}
for i := 0; i < len(strs); i++ {
fmt.Println(strs[i])
}
for _, i := range strs {
fmt.Println(i)
}
}
结构体
go中的结构体也与c++基本一致
结构体定义
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
go
type struct_variable_type struct {
member definition
member definition
...
member definition
}
例如:
go
type class struct {
members int
primaryTeacher string
major string
grade string
classNumber int
}
结构体赋值
可以使用结构体名.成员名
访问结构体成员赋值, 可以直接使用结构体名+{value1, value2, '''}
赋值,也可以使用结构体名+{结构体成员名:value'''}
赋值,没有赋值的会取默认值
例如:
go
package main
import "fmt"
type class struct {
members int
primaryTeacher string
major string
grade string
classNumber int
}
func main() {
class1 := class{35, "Feixin", "软件工程", "大二", 1}
fmt.Println(class1)
fmt.Println(class1.major)
var class2 class = class{major: "大数据"}
fmt.Println(class2.members)
fmt.Println(class2.major)
class2.members = 36
fmt.Println(class2.members)
}
输出结果为:
go
{35 Feixin 软件工程 大二 1}
软件工程
0
大数据
36
引用类型
指针
go语言中是有c语言中的指针的机制的,但是他为了安全,有种种限制,go语言中的一般指针是不能进行数学运算的。详情看深度解密Go语言之unsafe - 知乎 (zhihu.com)
- *类型:普通指针类型,用于传递对象地址,不能进行指针运算。
- unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。
- uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。
unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。
这里只介绍普通类型指针,与c指针类似,一样的使用&
取地址,使用*
取内容,但是不可以进行运算操作,换言之,其实就是在java
函数传参基本类型不方便的地方改成了c
,但是又不想像c
一样那么复杂又不安全就变成了这样。
使用起来与c语言相差不大,例如:
go
package main
import "fmt"
func main() {
a := 3
b := &a
*b = 2
fmt.Println(a)
}
输出结果为
go
2
与c不同的是,数组名不代表数组第一个元素的地址
go
package main
import "fmt"
func main() {
array := [5]int{1, 2, 3, 4, 5}
b := &array
fmt.Println(*b)
c := &array[0]
fmt.Println(*c)
}
输出结果为:
go
[1 2 3 4 5]
1
并且不能进行运算
Slice(切片)
Go 语言切片是对数组的抽象。Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
感觉是不是有点像c++中的vector?😶,slice 有长度和容量的概念,也类似于c++中的size 和 capacity
切片的定义
var sliceName []sliceType
其实就是[]
中没有数量的数组,或者使用make函数
例如:
go
var slice []int
// 使用make函数
var slice3 = make([]int, 5, 10)
slice4 := make([]int, 5, 10)
// 第三个参数可省去
slice5 := make([]int, 10)
使用make
函数时,参数分别为slice的数据类型、初始长度、容量,长度内的初始值默认为0,长度外容量内的值虽然有内存空间但不会被初始化,也不能使用。因为是动态数组,容量满了也可以继续增加元素,但是如果容量满了增加元素就会创建一个新的动态数组并把之前的元素全部copy其中,会耗费时间。
切片的初始化
go
package main
import "fmt"
func main() {
// 类似数组的方法:
var slice1 []int = []int{1, 2, 3}
slice2 := []int{1, 2, 3}
fmt.Println(slice1, slice2)
// 利用数组, 区间左闭右开
array := [5]int{1, 2, 3, 4, 5}
slice3 := array[1:] // 指定开头
slice4 := array[:3] // 指定结尾
slice5 := array[1:3] // 指定区间
slice6 := array[:] // 全部
fmt.Println(slice3, slice4, slice5, slice6)
// 利用切片
slice7 := slice6[1:3] // 上述利用数组的方法都可以,略过不表
fmt.Println(slice7)
}
结果为:
go
[1 2 3] [1 2 3]
[2 3 4 5] [1 2 3] [2 3] [1 2 3 4 5]
[2 3]
获取切片的长度和容量
可以使用len()
函数和cap()
函数获取切片的长度和容量
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("len = %d, cap = %d", len(slice), cap(slice))
}
结果为:
go
len = 5, cap = 5
添加元素(append)
既然是动态数组那么必然少不了我们的添加元素了!还是我们熟悉的append。
append内置函数将元素附加到切片的末尾。如果它有足够的容量,则重新划分目标以容纳新元素。如果没有,将分配一个新的底层数组。Append返回更新后的切片。
语法为append(slice, elements)
go
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice)
slice = append(slice, 2)
//也可以多个元素append
//比如append(slice, 2, 3, 4)
fmt.Println(slice)
}
结果为:
go
[1 2 3 4 5]
[1 2 3 4 5 2]
切片的访问
与数组一样,可以通过下标访问
go
func main() {
slice := []int{1, 2, 3}
fmt.Println(slice[2])
// 输出3
}
切片的遍历
可以使用下标遍历,也可以使用for-each遍历
go
package main
import "fmt"
func main() {
strs := []string{"qwq", "qaq", "=.="}
for i := 0; i < len(strs); i++ {
fmt.Println(strs[i])
}
for _, i := range strs {
fmt.Println(i)
}
}
map
与c++的map类似, 但略有不同
Map 是一种无序的键值对的集合。
Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。
在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 "" 。
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。
map的定义
- 使用make函数
make(map[keyType]valueType)
go
map1 := make(map[int]int)
var map2 = make(map[int]int)
- 使用map关键字
map[keyType]valueType{key1:value1, key2:value2, ...}
, 这里注意行末一定要打逗号
go
map1 := map[string]int{
"qwq": 1,
"=.=": 2, // 注意要打逗号
}
var map2 = map[string]int{
"qaq": 1,
"0.0": 2,
}
map的访问
与c++一样的是,我们可以通过map[key]
获取到value
值, 也可以通过key来修改value的值
go
package main
import "fmt"
func main() {
map1 := map[string]int{
"qwq": 1,
"qaq": 2,
}
fmt.Println(map1["qwq"])
map1["qwq"] = 3
fmt.Println(map1["qwq"])
}
输出结果为:
go
1
3
不同的是我们可以这样操作
go
package main
import "fmt"
func main() {
map1 := map[string]int{
"qwq": 1,
"qaq": 2,
}
a, ok := map1["qwq"]
b, flag := map1["=.="]
fmt.Println(a, ok)
fmt.Println(b, flag)
}
输出结果为:
go
1 true
0 false
这里第一个变量是返回的值,如果map中不存在该变量,返回的就为value类型的空值,比如这里int就返回0,第二个变量是返回map中是否存在该变量的bool值,如果存在则返回true,如果不存在则返回false
map的遍历
可以使用for-each range来遍历map
go
package main
import "fmt"
func main() {
map1 := map[int]int{
0: 48,
1: 49,
2: 50,
}
for key, value := range map1 {
fmt.Print(key, value)
fmt.Print(" ")
}
}
结果为:
go
0 48 1 49 2 50
map键值对的删除
我们可以使用delete()
函数来删除map中的键值对。
go
package main
import "fmt"
func main() {
map1 := map[string]int{
"qwq": 1,
"qaq": 2,
"=.=": 3,
}
delete(map1, "qaq")
for key, value := range map1 {
fmt.Println(key, value)
}
}
结果为:
go
qwq 1
=.= 3
函数与结构体方法
golang中的函数允许返回多个值,并且可以为结构体定义方法,类似于java中的类的成员函数
返回单个值函数定义为
func name(参数1, 参数2...) 返回值type {
}
func name(参数1, 参数2...) (返回值1, 返回值2...) {
}
例如:
go
package main
import "fmt"
type class struct {
major string
numbers int
teacherName string
}
func getTeacherName(cls class) string {
return cls.teacherName
}
func (c class) getTeacherName2() string {
return c.teacherName
}
func main() {
c1 := class{"bigdata", 40, "Feixin"}
fmt.Println(c1.getTeacherName2())
fmt.Println(getTeacherName(c1))
}
结果为:
go
Feixin
Feixin
空接口 interface
1. 空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
go
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
go
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
2. 类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。
这两部分分别称为接口的动态类型
和动态值
。
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
go
x.(T)
其中:
- x:表示类型为interface{}的变量
- T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
go
func main() {
var x interface{}
x = "码神之路"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:
go
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。