变量
代表可变的数据类型,定义方式如下
var i int = 10
var i = 10
var (
i int = 10
j int = 10
)
var (
i = 10
j = 10
)
i := 10
- 变量可以赋值,直接 i = 20 修改即可
- 变量定义了就必须使用
- Go 是静态类型语言,不能在运行期间改变变量类型
i := 10
i = true
是错的
i := 10
i = 11
是对的
常量
在编译期就确定好了,不能修改,常量只允许布尔型、字符串和数字类型
const name = "Mike"
const one = 1
const bf = true
指针
变量的内存地址,通过 & 获取指针,直接输出获取地址,加 * 获取指针指向的变量值
package main
import "fmt"
func main() {
i := 10
pi := &i
fmt.Println(*pi)
fmt.Println(pi)
}
引用数据类型
数组
数组是聚合类型,他们的值由内存中的一组变量构成,数组的元素具有相同的类型 静态数据结构,长度固定 内置函数 len 用于返回数组中元素的个数
package main
import "fmt"
func main() {
var a [3]int
// a := [3] int{}
fmt.Println(a[0])
fmt.Println(a[len(a)-1])
//输出索引和元素
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
//仅输出元素
for _, v := range a {
fmt.Printf("%d\n", v)
}
}
初始化的时候默认值是0
q := [...]int{1,2,3}
fmt.Printf("%T\n",q) // "[3]int"
如果是 [...] 则说明数组的长度由初始化数组的元素个数决定
q := [3] int{1,2,3}
q = [4] int{1,2,3,4}//出现编译错误
数组的长度在编译时就可以确定
array := [5] string{1: "b", 3: "d"}
初始化部分值
package main
import "fmt"
func main() {
array := [5]int{1, 2, 3, 4, 5}
for i, v := range array {
fmt.Println("数组索引:%d, 对应值:%s\n", i, v)
}
}
数组的遍历
var c [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
c[i][j] = i + j
}
}
fmt.Println("2d: ", c)
二维数组
切片
动态数据结构,长度可以动态增长。因为高效并且占用内存少,使用比较多 可以看成是对数组的一层简单的封装,因为每个切片的底层数据结构中,一定包含一个数组 有三个属性,指针、长度和容量,
- 容量实际上代表了它的底层数组的长度
- 长度指的是切片的长度
切片类似于一个窗口,通过窗口看东西,当我们用make函数或切片值字面量(比如[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素
但是切片代表的窗口无法向左拓展,也就是说,我们永远无法透过切片看到对应数组中最左边的那 3 个元素
有两种生成方式,其中一种是通过在原有数组的基础上生成切片,第二种是直接调用 make 函数生成
package main
import "fmt"
func main() {
//基于数组生成
array := [5]string{"a", "b", "c", "d", "e"}
slice := array[2:3]
slice1 := array[2:4]
fmt.Println(slice)
fmt.Println(slice1)
slice2 := make([] string. 4, 8)
fmt.Println(len(slice2), cap(slice2), slice2)
}
切片格式中,包含索引 start 但是不包含 end(start 和 end 都可以省略, start 省略默认 0, end 省略 默认数组长度, 都省略默认整个数组)
//基于 make 函数生成
//长度为5
s1 := make([]int, 5)
//长度为5,容量为8
s2 := make([]int, 5,8)
修改操作 对切片修改后,原数组也会被修改,也就是说基于数组的切片,使用的底层数组还是原来的数组,一旦修改切片的元素值,那么底层数组对应的值也会被修改 Append 会自动处理切片容量不足的扩容问题,只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。
//追加一个元素
slice2:=append(slice1,"f")
//多加多个元素
slice2:=append(slice1,"f","g")
//追加另一个切片
slice2:=append(slice1,slice...)
Append 函数总会返回新的切片,如果新切片的容量比原切片的容量更大,那么就说明底层数组也是新的
Map
动态数据结构,长度可以动态增长
func main(){
m := make(map[string]int) //使用make创建一个空的map
m["one"] = 1
m["two"] = 2
m["three"] = 3
fmt.Println(m) //输出 map[three:3 two:2 one:1] (顺序在运行时可能不一样)
fmt.Println(len(m)) //输出 3
v := m["two"] //从map里取值
fmt.Println(v) // 输出 2
delete(m, "two")
fmt.Println(m) //输出 map[three:3 one:1]
m1 := map[string]int{"one": 1, "two": 2, "three": 3}
fmt.Println(m1) //输出 map[two:2 three:3 one:1] (顺序在运行时可能不一样)
for key, val := range m1{
fmt.Printf("%s => %d \n", key, val)
/*输出:(顺序在运行时可能不一样)
three => 3
one => 1
two => 2*/
}
}
结构体
结构体是聚合类型,他们的值由内存中的一组变量构成,结构体的元素可以是不同的数据类型 静态数据结构,长度固定
语句
//if 语句
if x % 2 == 0 {
//...
}
//if - else
if x % 2 == 0 {
//偶数...
} else {
//奇数...
}
//多分支
if num < 0 {
//负数
} else if num == 0 {
//零
} else {
//正数
}
//Switch
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4,5,6:
fmt.Println("four, five, six")
default:
fmt.Println("invalid value!")
}
//经典的for语句 init; condition; post
for i := 0; i<10; i++{
fmt.Println(i)
}
//精简的for语句 condition
i := 1
for i<10 {
fmt.Println(i)
i++
}
//死循环的for语句 相当于for(;;)
i :=1
for {
if i>10 {
break
}
i++
}
注意:无论任何时候,你都不应该将一个控制结构(( if 、 for 、 switch或select )的左大括号放在下一行。如果这样做,将会在大括号的前方插入一个分号,这可能导致出现不想要的结果。
内存分配
有两种,分别是 new 和 make
- New
将内存清零,而不是初始化内存
- Make
用于创建切片、map 和管道,并且返回类型 T 的一个被初始化了的实例。
函数
用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。 函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。 函数签名就是参数列表和结果列表的统称,只要参数列表和结果列表一样就可以说是一样的函数
使用
package main
import "fmt"
type Printer func(contents string) (n int, err error)
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer
p = printToStd
p("something")
}
错误
Error 接口
只有一个 Error 方法用来返回具体的错误信息
type error interface {
Error() string
}
- 例子
这里面由于 a 无法转为数字,所以报错
func main() {
i,err:=strconv.Atoi("a")
// i,err := strconv.Atoi("10")
if err!=nil {
fmt.Println(err)
}else {
fmt.Println(i)
}
}
- 工厂函数:
自己定义的函数返回错误信息给调用者
func add(a,b int) (int,error){
if a<0 || b<0 {
return 0,errors.New("a或者b不能为负数")
}else {
return a+b,nil
}
}
这里使用 errors.New 这个工厂函数生成错误信息。它接收一个字符串参数,返回 error 接口 在调用的时候就可以直接使用
package main
import (
"errors"
"fmt"
)
func add(a, b int) (int, error) {
if a < 0 || b < 0 {
return 0, errors.New("a或者b不能为负数")
} else {
return a + b, nil
}
}
func main() {
sum, err := add(-1, 2)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(sum)
}
}
- 自定义 error
平时的错误信息只能传递一个字符串,如果想要携带更多信息(比如错误码信息),那么就需要自定义 error 其实就是先自定义一个新类型,比如结构体,然后让这个类型实现 error 接口
type commonError struct {
errorCode int //错误码
errorMsg string //错误信息
}
func (ce *commonError) Error() string{
return ce.errorMsg
}
- 错误的嵌套
package main
import (
"errors"
"fmt"
)
type MyError struct {
err error
msg string
}
func (e *MyError) Error() string {
return e.err.Error() + e.msg
}
func main() {
//err是一个存在的错误,可以从另外一个函数返回
e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误:%w", e)
fmt.Println(w)
}
结果
GOROOT=C:\Program Files\Go #gosetup
GOPATH=C:\Program Files\Go;C:\Program Files\Go\bin #gosetup
"C:\Program Files\Go\bin\go.exe" build -o C:\Users\Administrator\AppData\Local\Temp\GoLand___1go_build_awesomeProject__1_.exe C:\Users\Administrator\GolandProjects\awesomeProject\test_09.go #gosetup
C:\Users\Administrator\AppData\Local\Temp\GoLand___1go_build_awesomeProject__1_.exe
Wrap了一个错误:原始错误e
Deferred
可以保证文件关闭后一定被执行,不管是自定义函数的出现异常还是错误
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
//省略无关代码
return readAll(f, n)
}
以上面的 ReadFile 函数为例,被 defer 修饰的 f.Close 方法延迟执行,也就是说会先执行 readAll(f, n),然后在整个 ReadFile 函数 return 之前执行 f.Close 方法。
Panic
解决运行时的问题
func connectMySQL(ip,username,password string){
if ip =="" {
panic("ip不能为空")
}
//省略其他代码
}
如果是不影响程序运行的错误,不要使用 panic,使用普通错误 error 即可 场景:协程A执行过程中需要创建子协程A1、A2、A3...An,协程A创建完子协程后就等待子协程退出。 针对这种场景,GO提供了三种解决方案:
- Channel: 使用channel控制子协程
- WaitGroup : 使用信号量机制控制子协程
- Context: 使用上下文控制子协程
一个程序启动,就会有对应的进程被创建,同时进程也会移动一个线程,这个线程叫做主线程。如果主线程结束,那么整个程序就退出了。有了主线程,就可以在主线程程里启动很多其他线程,就有了多线程的并发 但是 Go 里面没有线程的概念,只有协程,相对于线程更加轻量,一个程序可以随意启动成千上万个协程,而协程是被 Go runtime 调度, Go 自己决定同时执行多少个协程,什么时候执行哪几个 Go 语言中提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是体长通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。