golang的个人学习笔记以及错题集

时间:2025-02-24 14:06:23

主要参考对象
/zh-cn/learn/paths/go-first-steps/
/

安装

/
下载安装即可

语言

工作区

Go 在组织项目文件方面与其他编程语言不同。 首先,Go 是在工作区的概念下工作的。工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一工作区,可以通过环境变量$GOPATH 修改。go 1.11 引入了 Go Modules模块,环境变量GO111MODULE = on时(不同版本含义有点不一样)可以将所有的代码仓库均不存储在 GOPATH 下。

在任意一个目录下执行 go mod init 会生成文件,之后就可以直接执行这个目录下的文件(类似这个目录就成了工作区)

常见目录结构

  1. bin
  2. src
  3. pkg

变量

使用var 声明变量

在没有值的情况下需要标注类型

var firstName string
var firstName, lastName string
var (
    firstName, lastName string
    age int
)

初始化变量

如果你决定初始化某个变量,则不需要指定其类型,因为当你使用具体值初始化该变量时,Go 会推断出其类型

使用冒号等于号时,要声明的变量必须是新变量,在声明函数外的变量时,必须使用 var 关键字执行此操作,即:=只能在函数内部使用,如果声明全局变量需要使用var关键字

如果声明了变量但未使用,Go 会抛出错误, 使用特殊字符 _ 可以占位,不使用,让编译顺利通过(一些函数会返回多个值,必须接收,但又没用)

var (
    firstName = "John"
    lastName  = "Doe"
    age       = 32
)

var (
    firstName, lastName, age = "John", "Doe", 32
)


func main() {
    firstName, lastName := "John", "Doe"
    age := 32
    (firstName, lastName, age)
}

常量

const HTTPStatusOK = 200
const (
    StatusOK              = 0
    StatusConnectionReset = 1
    StatusOtherError      = 2
)

数据类型

Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值.共有四类数据类型:

  1. 基本类型:数字、字符串和布尔值
  2. 聚合类型:数组和结构
  3. 引用类型:指针、切片、映射、函数和通道
  4. 接口类型:接口

在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。

基本类型

  1. 数字
  2. 字符串
  3. 布尔值
数字

int8、int16、int32(rune)、int64、uint8(byte)、uint16、uint32、uint64、float32、float64

默认值: 整数数值为0,浮点数为+0.000000e+000

定义整数类型的关键字是 int,但 Go 还提供了 int8、int16、int32 和 int64 类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int,则大小通常为 32 位。 在 64 位系统上,int 大小通常为 64 位。 但是,此行为可能因计算机而不同。 可以使用 uint。 但是,只有在出于某种原因需要将值表示为无符号数字的情况下,才使用此类型。 此外,Go 还提供 uint8、uint16、uint32 和 uint64 类型。

Go 语言把字符分 byte 和 rune 两种类型处理。 rune 是int32 的别名,常用于中文的对应的Unicode, byte 是uint8的别名,/cheyunhua/p/

如果尝试在不同类型之间执行数学运算,将会出现错误, 在 Go 中将值从一种类型转换为另一种类型时,需要显式声明新类型

布尔值

true、false

默认值: false

你可以使用关键字 bool 声明布尔类型。 Go 不同于其他编程语言,在 Go 中,你不能将布尔类型隐式转换为 0 或 1。 你必须显式执行此操作

字符串

string 用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号(")中定义值。 单引号(')用于单个字符

默认值: string类型空值

类型转化

使用内置函数
var integer16 int16 = 127
var integer32 int32 = 32767
(int32(integer16) + integer32)
使用 strconv 包
package main

import (
    "fmt"
    "strconv"
)

func main() {
    i, _ := ("-42")
    s := (-42)
    (i, s)
}

函数

函数包括一般的函数,以及特殊函数(方法):在函数名称之前加入一个额外的参数,此附加参数称为接收方。

方法的用处:分组函数并将其绑定到自定义类型(例如:结构)。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

函数

main()

Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 你的程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。

main() 函数没有任何参数,并且不返回任何内容。 如要访问 Go 中的命令行参数,可以使用用于保存传递到程序的所有参数的 os 包 和 变量来执行操作

自定义函数

语法: 关键字(func),自定义命名(name),参数(parameters),返回值(results:可以是函数内部的变量,这样return就不用待参数了;如果只是类型声明,则要明确返回数据;可以多个)

func name(parameters) (results) {
    body-content
}

方法

方法使用中的一些注意点:

  1. 使用指针:方法需要更新变量,或者,如果参数太大,则可能需要避免复制它。在遇到此类情况时,你需要使用指针传递变量的地址。另外依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方,即使某个方法不需要也是如此。
  2. 声明其它类型(非结构):可以为任何类型创建方法,但不能给其它包的类型定义方法。因此无法给基本类(例如:string)定义方法。可以在上面加装一层
package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return (string(s))
}

func main() {
    s := upperstring("Learning Go!")
    (s)
    (())
}
  1. 嵌入方法: 结构(A)中属性可以使用另一个结构(B),如果B存在方法(fc),则示例A可以直接调用该方法,有点类型基类、子类的继承关系。实际上Go 编译器会通过创建新的方法来实现。
func (t A) fc() int {
    return ()
}
  1. 重载方法:上述3中希望A的方法C与B不同,可以直接定义方法。要继续使用B的C方法,只能显性调用()
func (t A) fc() int {
    return something
}
  1. 封装(访问权限): 对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 private 或 public 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法。另外,Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。

接口

接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。

接口是一种抽象类型

  1. 声明接口
type Shape interface {
    Perimeter() float64
    Area() float64
}
  1. 实现接口
type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return  * 
}

func (s Square) Perimeter() float64 {
    return  * 4
}


type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return  *  * 
}

func (c Circle) Perimeter() float64 {
    return 2 *  * 
}


func printInformation(s Shape) {
    ("%T\n", s)
    ("Area: ", ())
    ("Perimeter:", ())
    ()
}


func main() {
    var s Shape = Square{3}
    printInformation(s)
    // 此时运行时可以自动转换类型,不报错
    c := Circle{6}
    printInformation(c)
}

  1. 实现字符串接口
  2. 扩展现有实现

Go 包与其他编程语言中的库或模块类似。 你可以打包代码,并在其他位置重复使用它。 包的源代码可以分布在多个 .go 文件中。

  1. 声明: 在文件中添加 package calculator ,声明这个go文件为calculator 包
  2. 导入: 在 Go 中,包名称需遵循约定。 包使用其导入路径的最后一部分作为名称。 例如,Go 标准库包含名为 math/cmplx 的包,该包提供用于处理复数的有用代码。 此包的导入路径为 math/cmplx
import "math/cmplx"

()

main包

在 Go 中,甚至最直接的程序都是包的一部分。 通常情况下,默认包是 main 包。如果程序是 main 包的一部分,Go 会生成二进制文件。 运行该文件时,它将调用 main() 函数。
换句话说,当你使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(扩展名为“.a”的文件)。

一般包

你现在可以开始编写包的函数和变量。 不同于其他编程语言,Go 不会提供 public 或 private 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:

  1. 如需将某些内容设为专用内容,请以小写字母开始。
  2. 如需将某些内容设为公共内容,请以大写字母开始。
    例如:
package calculator

var logMessage = "[LOG]"

// Version of the calculator
var Version = "1.0"

func internalSum(number int) int {
    return number - 1
}

// Sum two integer numbers
func Sum(number1, number2 int) int {
    return number1 + number2
}
  1. 只能从包内调用 logMessage 变量。
  2. 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
  3. 只能从包内调用 internalSum 函数。
  4. 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。

模块

可以将包放到模块中。 Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。

此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。

因此,若要为 calculator 包创建模块,请在根目录 ($GOPATH/src/calculator) 中运行以下命令:go mod init /myuser/calculator。运行此命令后,/myuser/calculator 就会变成模块的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 的新文件。

导入本地模块时,可以直接使用上述mod init的名称,再去生成的中修改指向

module helloworld

go 1.14

require /myuser/calculator v0.0.0

replace /myuser/calculator => ../calculator

其它

新手错题集

struct literal uses unkeyed fields

出现在结构实例的时候

  1. 检查结构体是否真的存在该字段
  2. 检查是否在包外实例结构,但字段名是小写开头(类似私有),需改为大写

missing type in composite literal

常出现在结构实例的时候,特别是嵌套时,偷懒导致

  1. 结构不能直接{}, 得先声明,类似这样 Structname{},嵌套内的结构也一样,偷懒会忘了

too few values in struct literal

  1. 如下所示:实例的时候 t 这种写法必须参数写全,不然会报错
type triangle struct {
    size int
}
type coloredTriangle struct {
	triangle
	color string
}

t := coloredTriangle{triangle{3}, "blue"}
t1 := new(coloredTriangle)
 = triangle{3}

t2 := coloredTriangle{
	triangle: triangle{3},
	color: "blue",
}

has no field or method

  1. 代码类似上面错误里的代码,这里主要是指嵌套结构的调用( Embed methods)时报错,如下所示:、()都会报错,原因在于定义coloredTriangle的triangle给了变量名(上面那个没有),调用的时候必须显性的这样
type triangle struct {
    size int
}
type coloredTriangle struct {
	a triangle
	color string
}

func (t triangle) perimeter() int {
    return  * 3
}

t := coloredTriangle{triangle{3}, "blue"}
("Size:", )
("Perimeter", ())

cannot assign 1 values to 2 variables 或者 cannot initialize 1 variables with 2 values

  1. 注意赋值的时候不要少了一个变量接收,特别是调用函数的时候,是有返回多个值的,例如 int, error , 必须两个变量接收

struct 类似类用法,但继承存在问题

如下所示,通过结构的嵌套实现类似class的效果,这种就相当于A为基类,定义一个值(很多方法需要这个值),这个值子类编写的会直接传入(好像没有直接给默认值的法子),可以在B实例后调用New_b来实现默认值

type A struct{
    a string
    b map[string]string
}

type B Struct {
    A
}

var map_b:= {
    "test_key": "test_value"
}

func (b B)New_b() B{
     = map_b
    return b
}

test_b := B{A:{a:"test_a"}}

cannot use a (variable of type any) as string value in argument

泛型使用(any要至少1.18以上): 直接作为函数入参使用变量会报错,得转以下,顺便判断下a, ok := a.(string)

string 字符串多行

  1. 使用反引号`` , 注意不要乱打空格制表

gkb utf 互转 (enconding)

  1. 使用官方的第三方包(绝了)
// /golang/text
// go get -u /x/text

import 	"/x/text/encoding/simplifiedchinese"
mb, _ := ().Bytes(b)

golang与其它语言的比较

python

基于个人想法,受限水平,可能存在错误

  1. 明显的例如:golang是强类型语言,golang没有类(严格来说并不是一个面向对象的语言,但是可以实现类似的功能,例如通过结构、接口等)
  2. goruntine 是 go语言的特色,用于并发操作。在代码书写,思考逻辑上,个人感觉更类似python的线程(thread),而不是协程(coroutine)。这两者都是为了实现并发处理。这意味这goruntine更容易编写(个人感觉),而python的coroutine编写需要显性的不停的调用await、 async。
# 线程
import threading

def do_something():
    do something ... 

do_thread = threading.Thread(target=do_something)
do_thread.start()
do other ... 


# 协程corountine
import asyncio

async def do_something():
    do something ...
    await ...
    do ...

asyncio.run(do_something())    
                         

python示例如上,咋一看thread和corountine差不多,实践中多corountine并行处理的遇上其中一个阻塞,会导致整体阻塞(协程的本质是自己主动切换执行代码,遇上等待就去执行其它代码),而thread基本不会。
以工作经验举例:开发一个tcp服务用于模拟设备行为
行为1: 发起tcp链接登录
行为2: 登录后发送心跳包
行为3: 登录后等待接收服务器数据,接收数据解析并应答
thread实现:非常简单,使用queue做线程间数据传输,event做一些流程控制。主线程实例socket执行到登录,起三个子线程,一个循环发送queue中的数据,一个定时生成心跳放置到queue中,一个循环接收数据,数据接收到后,将应答放置到queue中。主线程进行循环等待脚本退出信号。出现严重错误,如socket异常后,重新实例socket,销毁所有子线程重头再来。
corountine实现:基本也是类似,但要注意在子线程里阻塞的一些操作,不会影响其它线程,例如是一个阻塞操作(没收到数据会一直等待),我是通过设置超时,让销毁子线程的时候能够一段时间后关闭,而协程里不能直接这么用,阻塞行为会让所有并发协程一起卡住(不太会异步写法,还好及时发现定时发送慢了),当然asyncio库已经带了socket异步化的操作(reader, writer = await asyncio.open_connection(family=socket.AF_INET,sock=mysocket)…当时找了半天)。还有socket异常后的处理,相对线程方案来说也比较麻烦,总而言之,编写的时候需要更加注意。

package main

import (
	"fmt"
	"time"
)

func time_to_sleep(s int) {


	("%d before sleep 当前日期:%s\n", s, ().Format("2006-01-02 15:04:05.000"))
	(5 * )
	("%d after sleep 当前日期:%s\n", s, ().Format("2006-01-02 15:04:05.000"))

}

func main() {
	start := ()
	for i := 1; i <= 3; i++ {
		go time_to_sleep(i)
	}
	elapsed := (start)
	("Done! It took %v seconds!\n", ())
	(10 * )
	("after all 当前日期:%s\n", ().Format("2006-01-02 15:04:05.000"))
}

Done! It took 0 seconds!
1 before sleep 当前日期:2022-08-04 16:49:23.643
3 before sleep 当前日期:2022-08-04 16:49:23.643
2 before sleep 当前日期:2022-08-04 16:49:23.643
2 after sleep 当前日期:2022-08-04 16:49:28.665
1 after sleep 当前日期:2022-08-04 16:49:28.665
3 after sleep 当前日期:2022-08-04 16:49:28.665
after all 当前日期:2022-08-04 16:49:33.655

结果输出代码结果输出如上所示,整体类似python的thread效果,还有,注意主线程是不管goruntine的,一旦结束,goruntine会直接结束,所以要等待goruntine完成,这里是直接等待,实际使用可能会使用channel或者 sync 的 WaitGroup