基础及关键字
if for switch都支持使用隐形声明(:=)来快速声明一个变量,无需在上面一行额外声明,这可以增加代码简洁性,但不太符合其他常规语言的写法,需要习惯一下
if for switch都不需要使用()包裹表达式,只使用空格隔开就行
大写的函数或变量才算是对外声明(用开头大写来代替其他语言中的
public private\export
的概念)基础类型转换是显式的,使用类型本身加(),值写在括号中,即可完成类型转换
导包和声明变量都支持使用()批量声明,这可以增加代码简洁性,也可以像其他语言一样一条条声明,也没问题
switch的case匹配到后就直接停止,不会像其他语言一样一路向后继续匹配,除非以
fallthrough
语句结束for关键字支持省略声明、条件、表达式三个部分,这直接代替别的语言中的while了
声明变量、方法参数、方法返回值时当多个参数类型重复时都支持省略类型,这可以有效减少重复代码,但也容易出bug,写法:
x, y, z int
go中各种表达式都使用分号;隔开
方法return支持多值return,只要方法的return类型定义时多定义几个就好了,小括号括起来,用逗号隔开
尽量避免使用隐式return(方法需要返回值时,直接一个return将函数中的所有变量都返回掉),因为隐式return容易出现问题(太依赖编译器,函数后续不好修改)
go中的返回值也支持声明类型的同时给其命名,这就更能说明不要使用隐式return了,容易出现问题
循环中使用
break
跳出defer关键字是defer语句所在的函数运行完成后再调用被defer修饰的代码,defer调用的函数会被压栈,会遵从后进先出的顺序依次调用
以下函数很违反直觉:
package main
import "fmt"
func main() {
fmt.Println("counting")
defer fmt.Println("done")
i := 0;
for ; i < 10; i++ {
defer fmt.Println(i)
}
}
结果:
counting
9
8
7
6
5
4
3
2
1
0
done
指针
- 指针直接指向内存地址,操作指针等于操作该值本身。
- 使用
&
对某个变量产生一个指针:&i
,使用*
修饰T(类型)来声明一个指针:p *int
,使用*p
的方式来解引用指针(使用指针)。 &
表示取地址,A变量被取地址后赋值给新的变量B,这个变量B就成为了变量A的指针,直接打印变量B时只会打印出内存地址*
表示解引用,变量B目前是指针,使用*
对变量B进行解引用,即*B
就可以打印原来的变量A的值,解引用后的*B
和原来的A的值是完全一样的,即内存上存储使用的是同一份。- 列表指针写法:
*List[T]
,指针的指针写法:**List[T]
,指针的指针是要修改的变量的指针的指针,不常用但是在一些特殊场景很有用,通常用在替换要修改的变量的指针本身时使用,口述比较绕,但是看一个关于链表的代码例子就明白了:
// 在 main 函数中,我们的链表头是一个指针
var listHead *List[T] = nil // 一开始是空链表
// 我们希望 Push 函数能修改 listHead 这个指针,让它指向一个新的节点。
func (l **List[T]) Push(v T) {
// 如果我们只接收 *List[T],那么 l 只是 listHead 的一个副本。
// 我们需要修改 listHead 本身,所以必须接收它的地址,即 **List[T]。
// *l 的意思就是:“通过这把钥匙,拿到 main 函数里的那张原始藏宝图(listHead 指针)”
// 然后我们修改这张原始藏宝图,让它指向一个新的宝藏(新节点)。
*l = &List[T]{next: *l, val: v}
}
- 黄金法则:想修改什么,就向函数传递它的指针,不论是变量还是指针本身
- 直接给指针本身赋值时,必须也要赋值指针,不能赋值值本身
结构体
- go中声明结构体(类似于类)使用
type 结构体名称 struct
加大括号:
type Vertex struct {
X int
Y int
}
- 实例化结构体:
v := Vertex{1, 2}
//允许如下方式实例化结构
v := Vertex{X: 1} //对X赋值使用冒号而不是等号,这里Y就是0,println(v)打印结果是 {1 0}
- 配合结构体使用:
p := &v
等价于
var p *Vertex = &v
go允许隐式解引用,即直接省略和星号和大括号对结构体引用进行使用:
p.X = 1e9
等价于
(*p).X = 1e9
数组和切片
- 数组声明方式:
var a [2]string
,与其他语言不同的是数量和中括号放在了类型前边 - 初始化数组的方式:
var a [2]string
,带初始值的方式:a := [2]string{"hello", "world"}
或者var c [2]string = [2]string{"hello", "world"}
- 数组和切片的区别是数组声明时必须指定长度,而切片则由编译器推导。数组是实际存储值的,而切片不存储值(只是相当于数组中某段元素的引用),切片更常用
- 切片遵从要头不要尾,
a[1:4]
表示取数组的1,2,3下标的元素,不包含4 - 切片类似于数组的引用,即它描述了数组中的某段元素,修改切片等于直接修改数组本身
- 构建数组时中括号
[]
里不写数字,就相当于构建了一个数组+切片,注意,这样的话长度其实仍然固定,如果直接给超过声明长度的下标赋值或取值就会报越界错误 - 切片可以忽略上界和下界:
//切片下界的默认值为 0,上界则是该切片的长度。
//对于数组 var a [10]int 来说,以下切片表达式和它是等价的:
a[0:10]
a[:10]
a[0:]
a[:]
make
(内置函数)用来创建动态数组,比较常用:
//第三个参数cap可以省略,也可以手动指定
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
- 数组的len可以理解为数组长度,cap可以理解为容量。容量总是大于等于长度的。
- 切片的类型可以是任意类型包括结构体或切片等,例如
[][]string
,[]struct
- append(内置函数)用来为切片追加值:
v := []int{0,1} //声明切片
v = append(v, 2, 3, 4) //追加多个值
fmt.Println(v) //结果:[0 1 2 3 4]
- 使用range关键字遍历切片或数组
- 在go中利用切片结合uint8的对应灰度值来渲染一张图片,关键点就是对切片本身的使用:
package main
import (
"golang.org/x/tour/pic"
)
func Pic(dx, dy int) [][]uint8 {
outerSlice := make([][]uint8 ,dy)
for y := range outerSlice {
innerSlice := make([]uint8, dx)
for x := range innerSlice {
v := x*x+y*y //改变这里可以改变结果图片的效果,例如:(x+y)/2、x*y、x^y、x*log(y) 、x%(y+1)、x*x、y*y 、x * math.Log(float64(y))等
innerSlice[x] = uint8(v)
}
outerSlice[y] = innerSlice
}
return outerSlice
}
func main() {
pic.Show(Pic)
}
map(映射)
- 创建map可以使用make函数,声明map类型时的格式如:
map[string]string
,第一个中括号中的为键类型可以是任意类型,通常是string,后面紧跟着的是值类型,可以是任意类型 - map创建时后面的大括号类似于kotlin中的mapOf,不过把to换成了冒号
- 通过双赋值表达式检测键是否存在:
v, exsit := m["答案"]
fmt.Println("值:", v, "是否存在?", exsit)
函数(方法)
- 函数可以作为参数传递,参数类型为func,函数可以赋值给一个变量,该变量即成为函数本身,跟js中的函数特点有点像,函数类型也可以作为另一个函数的返回值
- 函数的闭包通常可以用于统计和累计,这很有用,可以省去一些不必要的外部全局变量
- 一段不正确的斐波那契数列计算代码,这段代码跳过了开头的0和1的输出:
package main
import "fmt"
// fibonacci 是返回一个「返回一个 int 的函数」的函数
func fibonacci() func() int {
r := []int{0,1}
return func () int {
sum := r[len(r)-1] + r[len(r)-2]
r = append(r,sum)
//fmt.Println(r)
//fmt.Println( r[len(r)-1:len(r)][0] )
return sum
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Print(f()," ")
}
}
//会打印 :1 2 3 5 8 13 21 34 55 89
修正思路为每次调用时,第一个值正是要返回的结果,而后只使用最新的两个数字来保持状态值最新,下面是修正后的函数:
func fibonacci() func() int {
a,b := 0,1
return func () int {
ret := a
a,b = b,a+b
return ret
}
}
//打印:0 1 1 2 3 5 8 13 21 34
- 为结构体(或基础类型)定义方法的方式就是使用括号包含具体的带名称的类型,类似于kotlin的扩展方法,但是不能定义在结构体内,也必须得声明一个名称来引用而不是this
//定义
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//使用
v := Vertex{3, 4}
fmt.Println(v.Abs())
为类型定义方法的方式是使用type转一下类型:
//定义
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
//使用
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
- 不能跨包定义方法
- 为指针类型接收者定义方法通常更常用,因为有时需要通过该方法来修改结构体的数据并使其在后续生效:
package main
import (
"fmt"
)
type Vertex struct {
A,B int
}
//没有对v(指针型类型接收者)进行修改的能力
func (v Vertex) Sum() int {
return v.A+v.B
}
//有对v进行修改的能力
func (v *Vertex) Scale(l int) {
v.A = v.A * l
v.B = v.B * l
}
//使用
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Sum())
}
//打印70
- 如果不确定,或者想修改接收者,用指针接收者通常是更安全、更通用的选择,定义为指针接收者通常对性能也有很大的帮助,可以避免复制开销
- go的语法糖:1.定义为指针型接收者的方法,在调用时也直接允许值类型的进行调用 2.定义为值型接受者的方法,在调用时也允许指针类型进行调用
- 当一个方法实现了接口或者作为结构体的方法,有入参,有多个响应值,且响应值中有方法类型时,这个方法看起来会很奇怪,有多达4个或更多的括号,但是却有效合法,如下复杂示例:
package main
import "fmt"
type Vertex struct{
X,Y int
}
//Vertex类型的专属异常
type VertexError struct {
V Vertex
}
//Vertex类型的专属异常处理
func (e *VertexError) Error() string {
return fmt.Sprintf("vertex error on value: %+v", e.V)
}
//复杂函数
func (v *Vertex) Sum(scale func(int) int) (int,(func(int) int), error){
if v.X == -1 {
return 0, nil, &VertexError{*v}
}
return scale(v.X) + scale(v.Y),scale,nil
}
func Scale(s int) int {
return s * s
}
func main() {
v := Vertex{-1,2} //-1时会触发异常
s,f,err := v.Sum(Scale)
if err != nil {
fmt.Printf("error: %v",err)
return
}
fmt.Printf("s: %v f: %v",s,f)
}
接口
- go中的接口使用interface关键字,没有显示的类似于其他语言中的"implements"或冒号等定义,某个类型的方法只要包含目标接口的全部方法,就可以说该类型实现了目标接口,没有代码上显式的引用和定义关系
- 弊端是无法一眼看出某个类型到底实现了某个接口没有,好处就是代码灵活
- 接口也是值,可以在方法参数、返回值、变量等进行传递
- 空接口是一个特殊定义:
interface {}
,可以保存任何类型的值内容,类似于kotlin中的Any?或java中的Object+null类型 - 类型断言,格式和写法:
i.(string)
,具体用法如下,可以检查出该接口底层保存的值以及是否使用的是对应的类型:
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) //hello
s, ok := i.(string)
fmt.Println(s, ok) //hello true
f, ok := i.(float64)
fmt.Println(f, ok) //0 false
f = i.(float64) // panic
fmt.Println(f) //panic: interface conversion: interface {} is string, not float64
用来检查并获取值确实不错,看起来挺好用的
- 类型选择,格式和写法:
i.(type){ case ... }
, 类型选择的写法如下,可以通过类似switch的方式检查对应接口值是否是该类型:
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("是int类型")
case string:
fmt.Println("是string类型")
default:
fmt.Println("未知类型", v)
}
}
func main() {
do(21) //是int类型
do("hello") //是string类型
do(true) //未知类型 true
}
- 方法调用的返回值可以主动的响应error,通过判断error是否为空:
if err != nil
来进行错误处理:
package main
import (
"errors"
"fmt"
)
func Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("b is 0!") //抛出一个错误
}
return a / b, nil
}
func main() {
div, err := Div(3, 0)
if err != nil {
fmt.Println(err) //b is 0!
return //提前返回
}
fmt.Println(div)
}
- error是一个内置接口,可以轻松使用方法对某个类型定义特定的错误处理,实现:
Error() string
方法即可 - 错误处理完后应当立即返回,通常使用return或者log.Fatal
- 实现接口可以做很多事情,一种常见的好处就是解耦且不用关注额外细节就能实现强大的功能,因为方法参数如果是接口类型的,那么传入的实例只要能实现对应接口的所有方法,就可以正确作为参数,至于方法内部如何,应该根据方法实际情况来完成实现
- 实现了图片接口的结构体,可以借助pic库完成图片绘制,耦合性很低,表现力很高,示例:
package main
import (
"golang.org/x/tour/pic"
"image"
"image/color"
)
// Image 是我们将要定义的自定义图片类型。
// 它是一个空结构体,因为我们不需要存储任何数据。
// 图像的像素是动态计算出来的。
type Image struct{}
// Bounds 方法返回图像的尺寸。
// 我们定义一个 256x256 像素的图像。
func (i Image) Bounds() image.Rectangle {
return image.Rect(0, 0, 256, 256)
}
// ColorModel 返回图像的颜色模型。
// 我们使用标准的 RGBA 模型。
func (i Image) ColorModel() color.Model {
return color.RGBAModel
}
// At 方法是核心。它在给定的 (x, y) 坐标处返回一个颜色。
// 渲染程序会为图像中的每一个像素调用一次此方法。
func (i Image) At(x, y int) color.Color {
// 使用 x 和 y 坐标通过一个简单的函数生成一个值。
// 这里的 x^y 是按位异或(XOR)操作,能产生有趣的图案。
v := uint8(x ^ y)
// 返回一个 RGBA 颜色。
// R(红)= v, G(绿)= v, B(蓝)= 255, A(透明度)= 255(不透明)
// 这会产生一个蓝色的、带有渐变纹理的图像。
return color.RGBA{v, v, 255, 255}
}
func main() {
// 创建我们自定义 Image 类型的一个实例。
m := Image{}
// pic.ShowImage 会接收任何实现了 image.Image 接口的类型,
// 并将其渲染成图片(在 Go Tour 中是 base64 编码的 PNG)。
pic.ShowImage(m)
}
类(泛)型参数
- 类似于其他语言中的泛型
- 声明方式:
func Index[T comparable](s []T, x T) int
,这表示s的元素类型可以是满足了comparable
接口的所有类型,x 的类型也是满足了 comparable接口的所有类型
杂项
- 遍历链表:
//常见的需要取值的标准写法:
for n := l; n != nil; n = n.next
//将链表拨动到最后位置的写法:
current := *l
for current.next != nil {
current = current.next
}
- 构建字符串使用strings.Builder,方法是WriteString,获取结果是string()