【基础】go基础学习笔记

发布于:2025-07-26 ⋅ 阅读:(14) ⋅ 点赞:(0)
基础及关键字
  • 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()

网站公告

今日签到

点亮在社区的每一天
去签到