本文章也有对应的视频讲解:1小时快速了解Go语言
开发环境搭建
Go语言下载地址:https://go.dev/dl,Windows、Mac、Linux都支持,Windows和Mac下载后直接双击安装即可,Linux下载后解压到任意目录都可以,Linux需要手动设置环境变量GOROOT/GOPATH/PATH。
IDE使用Vscode即可,下载地址:https://code.visualstudio.com/Download,在vscode里安装一个扩展,名叫“Go”,出自Go Team at Google。
写第一段Go代码:
package main
import "fmt"
func main() {
var a float64
fmt.Printf("the value of a is %f\n", a)
}
保存上述go代码,文件名必须以.go结尾,运行go代码:
go run xxx.go
main()函数是程序的唯一入口,且main()函数必须位于package main中,一个目录下只能存在一个main()函数。
import里指明需要引用的其他package。
Printf()是格式化输出函数,其位于fmt这个package下,%f是浮点数的占位符。
在Go语言里左大括号{
统一置于行尾,不能另起一行书写。
基础数据类型
变量声明:
var a int // 此时a=0
任何时候,Go语言总是变量名在前,类型在后。其他常用的基础数据类型还有int32、int64、float32、float64、string、bool。
变量初始化,即第一次给变量赋值:
var a int = 1
var a = 1 // 根据值推断类型
a := 1 // 省略var,用:代替
修改变量的值:
a = 2
强制类型转换:
var a float64
var b int64
var c int
c = int(b) // int64不会自动转为int
a = float64(c) // int不会自动转为float64
指针:
a := 3
var b *int = &a // &是取址符号,*int表示指向int的指针类型
fmt.Printf("a的地址是%p\n", b) // 格式化输出地址使用%p
var c int = *b + 7 // 指针前面加个*,把指针类型转为原始的数据类型
fmt.Printf("a+7=%d\n", c)
函数
func swap(a int, b int) (c int, d int){
c, d = b, a
return
}
func关键字表示后面要定义一个函数。
函数可以有0个或多个参数,这里指a和b。
函数可以有0个或多个返回值,这里指c和d。
package main
import (
"fmt"
"time"
)
func work() {
begin := time.Now() //当前时间
defer func() { //defer后面跟匿名函数
diff := time.Now().Sub(begin) //计算时间差
fmt.Printf("用时%d毫秒\n", diff.Milliseconds())
}() //小括号里用于传递参数值,匿名函数参数为空,所以这里没有传值
time.Sleep(2 * time.Second) //休眠2秒钟
}
defer后面跟的代码或匿名函数,在主函数(这里指work())临退出之前才执行。
结构体
定义结构体:
type User struct{
name string
age int
}
创建结构体实例:
u1 := User{name: "大乔乔", age: 18}
u2 := User{age: 18} // name为空字符串
u3 := User{} // name为空字符串,age为0
读写结构体的成员变量:
u3.age = 28
var a int = u1.age + u2.age
结构体可以拥有0个或多个成员方法:
func (u User) GetAge() int {
return u.age
}
func (u *User) SetAge(a int) {
u.age = a
}
成员方法其实等价于普通函数:
// 函数参数传递的是User的拷贝
func GetAge(u User) int {
return u.age
}
// 函数参数传递的是User的指针
func SetAge(u *User, a int) {
u.age = a
}
调用结构体的成员方法:
u3.SetAge(u2.GetAge())
Go语言里不存在静态成员,所以成员变量和成员方法只能通过结构体的实例来访问。
Go语言不存在继承,但是可以把一个结构体作为另一个结构体的成员变量的类型。
type Leader struct{
TeamSize int
human User
}
var l Leader // 声明结构体变量后,其成员变量均为空值
l.human = User{name: "大乔乔", age: 18}
l.human.SetAge(l.human.GetAge()+10)
接口
接口是一组方法的集合。
type Animal interface{
GetAge() int
SetAge(int)
}
凡是具备GetAge和SetAge这两个方法的结构体都实现了Animal接口,比如上文的User结构体,即可以认为User是一种Animal。
var a Animal
a = u1
也就是说GetAge和SetAge是成为Animal的准入条件,那如果一个接口里没有任何方法(空接口),就可以认为任意类型都实现了这个接口。Go语言自带了一个空接口类型--any,任何数据类型都属于一种any。
type any interface{}
var b any
b = u1
b = false
b = 9
b = "golang"
结构体实现接口,不需要在定义结构体时显式使用implements等关键字。
函数参数、结构体的成员变量都可以是接口类型。
func getAge(a Animal) int{
return a.GetAge() + 10
}
type Arm struct{
Member Animal
country string
}
func (arm Arm) GetAge () int {
return arm.Member.GetAge() + 10
}
引用数据类型
切片
切片的定义
type slice struct {
array unsafe.Pointer // 底层数组的指针
len int
cap int
}
由于切片持有一个数组的指针,我们称切片“引用”了一个数组,所以切片是一种引用数据类型。
arr := make([]int, 3, 5) // 切片指向一个int数组,长度为3,容量为5
arr[0], arr[1], arr[2] = 2, 9, 7 // 给切片的3个元素赋值
brr := arr // 切片的赋值拷贝
注意brr:=arr
,go语言里的所有等号赋值都会发生拷贝,由于切片是一个包含3个成员变量的结构体,所以切片的拷贝实际上拷贝的是那3个成员变量,也就是说并没有拷贝底层数组,而只是拷贝了底层数组的指针。brr[0]=3
把底层数组的首元素修改成了3,此时arr[0]
也变成了3。
切片的长度可以增加:
arr = append(arr, 5) // arr的长度为4,brr的长度还是3,即brr[3]会发生数组越界,而arr[3]没问题
新元素5追加到7后面,即放到预分配内存里。注意append函数并不会修改切片的长度,只是它会返回一个新切片,新切片的长度增加了。
通过多次append如果预分配内存填满了,则go会自动申请一块更大的底层数组,把老数组的元素逐一拷贝到新数组里去。
arr = append(arr, 5) // arr的长度为5,brr的长度还是3。注意此时预分配内存已填满
arr = append(arr, 5) // arr的长度为6,容量为10。arr指向新的更大的数组,brr还指向老数组
切片的遍历有两种方式:
var sum int // 此时sum的值是0
for i:=0;i<len(arr);i++{
sum += arr[i]
}
sum = 0
for i,ele := range arr{
_ = i // 变量i声明了就必须使用,这里把它赋给_,_相当于占位符,_无法被读取
sum += ele
}
fmt.Println(sum)
map
map也是一种引用数据类型,它引用的是一个哈希表HashTable。
map的声明和赋值:
var m map[int]string //声明map变量,key是int类型,value是string类型
m := map[int]string{3:"abc", 7:"mmj"} //声明并初始化
m[5]="hsi" //往map里添加一对key-value,也可能是更新key对应的value
delete(m, 3) //从map里删除key及其对于的value
map的遍历:
for key, value := range m{
fmt.Printf("key=%d, value=%s\n", key, value)
}
channel
channel也是一种引用数据类型,它引用的是一个环形队列,队列意味着先进先出。
声明并初始化channel变量:
ch := make(chan int, 10) //队列里存在int类型的元素,队列容量为10
读写channel:
ch <- 1 //向队列尾部添加元素1。如果队列已满,则此操作会阻塞
a := <-ch //从队列首部取走一个元素,并赋给变量a。如果队列已空,则此操作会阻塞
遍历channel:
close(ch)
for ele := range ch{
fmt.Println(ele)
}
注意遍历channel之前一定要先close它,表示不允许再向队列里写入元素,否则遍历channel的for循环可能一直无法退出甚至发生死锁。
并发
Go语言的并发性能非常优异,Go语言使用协程而非线程并发执行任务,协程比线程占用的内存空间更小,协程的创建、销毁、切换成本也更低。
go语言启动一个协程非常方便,应该是所有语言里面最方便的:
// 没有返回值的函数
func work(arg string){
// do something
}
go work("abc") //启动一个协程,异步地去执行work函数
// 或者直接在go关键字后面跟匿名函数
go func(arg string){
// do something
}("abc")
其它语言通过类似于join的函数等待线程结束,go语言里等待一个协程运行结束会比较麻烦。
package main
import (
"fmt"
"time"
)
func main() {
var result int
ch := make(chan bool)
go func() { //启动子协程
time.Sleep(2 * time.Second)
result = 5 //go语言不能获得子协程的返回值,只能通过一个公共变量来传递结果
ch <- true
}()
<-ch //channel为空,读操作阻塞。子协程结束后写channel,此时读channel才解除阻塞,从而实现了main协程等子协程结束
fmt.Println(result)
}
文件读写
// 读取一个文件的全部内容,写入另外一个文件
func CopyFile() {
fin, err := os.Open("a.go") //打开文件,准备读它
if err != nil {
log.Fatalf("打开文件a.go失败: %v\n", err)
}
defer fin.Close() //用完之后,记得关闭文件
fout, err := os.OpenFile("b.go", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) //打开文件,准备读它
if err != nil {
log.Fatalf("打开文件b.go失败: %v\n", err)
}
defer fout.Close() //用完之后,记得关闭文件
buffer := make([]byte, 1024) //创建长度为容量均为1024的byte切片,作为数据中转站
for { //相当于while true,go语言里没的while关键字
n, err := fin.Read(buffer) //从文件中读取1024个字节,放入buffer,n是实际读出的字节数,仅最后一次Read会发生n<1024的情况
if err != nil {
if err == io.EOF { //EOF(End Of File)表示来到文件末尾
if n > 0 {
fout.Write(buffer[0:n]) //[0:n]表示截取切片的前n个元素
}
} else {
fmt.Printf("读文件异常: %v\n", err)
}
break
} else {
fout.Write(buffer[0:n]) //把中转站里的内容写到一个文件里去
}
}
}
依赖管理
包package是go语言里面组织代码的最小单元,注意不是go文件,通常一个目录下的所有go文件的package必须一致,除非另一个package名以_test结尾。一个包下的所有go文件直接合并到一个go文件里(仅删除第一行的package xxx)不会有任何语法错误,这也意味着一个包内不能定义同名的全局变量、结构体、接口、函数(init()函数除外)。
若干个包组成一个模块module,module文件在go.mod文件里指定。go.mod里同时指定了go的版本号,以及本module依赖的所有其他module。
以大写开头的全局变量(即函数外的变量)、函数、结构体、结构体的成员变量、结构体的成员方法、接口可以在其他包中使用,称为“可导出”,类似于其他语言里public的概念。
/--project_root_path
|
/--dir1
| |
| /--a.go
|
/--dir2
| |
| /--b.go
|
/--go.mod
go.mod
module dqq/1h
go 1.22.0
通过go mod init dqq/1h
命令可以自动生成上述文件。
a.go
package util
// 可导出函数
func GetAge() int {
return 1
}
b.go
package main
import "dqq/1h/dir1" // {module}/{path}
func main(){
util.GetAge() // 调用函数时需要在前面加个包名。如果是访问本包内的函数、变量不加包名
}
如果import的内容不是go语言标准库里的包(即不在$GOROOT/src目录下),且module名称也不是当前module,则会去$GOPATH/pkg/mod目录下去搜寻相应的module。通过go get $module_name
命令会去互联网上下载对应module的源代码,放到本地的$GOPATH/pkg/mod目录下,并添加到go.mod文件的require条目里。
一个包下可以有多个init()函数,当import这个包时会去执行该包下的所有init()函数,执行main()函数之前会执行执行package main里的所有init()函数。
数据库编程
go语言官方只写了读写数据库的接口,并没有写任何具体的实现。所以针对各种数据库的读写只能依赖第三方库,而很多第三方库并没有按照go官方的接口来实现。github.com/go-sql-driver/mysql
按照go官方接口的约定,实现了针对mysql的读写。
先下载这个库:
go get github.com/go-sql-driver/mysql
package main
import (
"database/sql"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql" // 注册接口实现
)
func CheckError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "fatal error: %s\n", err.Error()) //stdout是行缓冲的,他的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出
os.Exit(1)
}
}
func main() {
//DSN(data source name)格式:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
db, err := sql.Open("mysql", "tester:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai")
CheckError(err)
defer db.Close()
rows, err := db.Query("select id,name,city,score,enrollment from student where enrollment>=20220703 limit 5") //查询得分大于2的记录
CheckError(err)
defer rows.Close()
for rows.Next() { //没有数据或发生error时返回false
var id int
var score float32
var name, city string
var enrollment time.Time
err = rows.Scan(&id, &name, &city, &score, &enrollment) //通过scan把db里的数据赋给go变量
CheckError(err)
fmt.Printf("id=%d, score=%.2f, name=%s, city=%s, enrollment=%s \n", id, score, name, city, enrollment.Format("2006-01-02 15:04:05"))
}
}
http编程
package main
import (
"net/http"
)
func home(w http.ResponseWriter, request *http.Request) {
// 从request里可以取得http请求体和请求头
w.Header().Set("key", "value") //先设置响应头
w.Write([]byte("Welcome")) //后设置响应体
}
func main() {
http.HandleFunc("/", home) //路由,请求要目录时去执行HelloHandler
if err := http.ListenAndServe("127.0.0.1:5678", nil); err != nil { //ListenAndServe如果不发生error会一直阻塞。为每一个请求单独创建一个协程去处理
panic(err) //打印错误信息和调用堆栈,以状态2结束进程
}
}
打开浏览器,在地址栏输入http://localhost:5678/
,查看响应体和响应头。
grpc编程
首先需要编写proto文件。
echo.proto
syntax="proto3";
option go_package = "./idl;idl"; //分号前是go文件的输出目录,分号后是go文件的package名
message Content {
string data = 1;
}
service EchoService {
rpc Echo(Content) returns (Content);
}
通过以下命令将.proto文件转为go文件。
protoc --go_out=. --go-grpc_out=. --proto_path=. echo.proto
服务端代码:
package main
import (
"context"
"dqq/1h/idl"
"net"
"google.golang.org/grpc"
)
type MyEcho struct {
idl.UnimplementedEchoServiceServer //EchoServiceServer接口要求所有的实现必须内嵌一个UnimplementedEchoServiceServer,用于将来的扩展
}
func (s MyEcho) Echo(ctx context.Context, request *idl.Content) (*idl.Content, error) {
if request == nil {
return nil, nil
}
return &idl.Content{Data: request.Data}, nil //返回结构体指针,加个&符号。error是个接口,可以用nil赋值
}
func main() {
// 监听本地的5678端口
lis, err := net.Listen("tcp", "127.0.0.1:5678")
if err != nil {
panic(err)
}
//创建服务
server := grpc.NewServer()
// 注册服务的具体实现
idl.RegisterEchoServiceServer(server, &MyEcho{})
// 启动服务
err = server.Serve(lis)
if err != nil {
panic(err)
}
}
客户端代码:
package main
import (
"context"
"dqq/1h/idl"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
//连接到服务端
conn, err := grpc.NewClient(
"127.0.0.1:5678",
grpc.WithTransportCredentials(insecure.NewCredentials()), //身份认证Credential即使为空,也必须设置
)
if err != nil {
fmt.Printf("dial failed: %s", err)
return
}
//创建client
client := idl.NewEchoServiceClient(conn)
request := &idl.Content{Data: "大乔乔"} //构造请求
resp, err := client.Echo(context.Background(), request) //发起grpc调用
if err != nil {
fmt.Printf("rpc failed: %v\n", err)
} else {
fmt.Println(resp.Data) //打印调用结果
}
}
进阶内容
enjoy your golang travels! 如需快速、高效、深入的学习Go语言,欢迎试听我录制的golang视频课程《golang从入门到通天》(电脑端按Ctrl++放大页面观看)
标签:err,int,转职,var,golang,go,func,Go,main From: https://blog.csdn.net/OrisunZhang/article/details/142252904