通过示例学习-Go-语言-2023-三十六-

通过示例学习 Go 语言 2023(三十六)

理解 Go (Golang) 中的 Println 函数

来源:golangbyexample.com/println-golang/

![println](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/5327d9a5221455aba6ab8a7076afdfd2.png)

目录

  • 概述

  • 程序

概述

Println 定义在 fmt 包中,用于格式化字符串并写入标准输出。

golang.org/pkg/fmt/#Println

以下是 Println 的函数原型。

func Println(a ...interface{}) (n int, err error)

Println 使用默认格式说明符格式化字符串,并在字符串后添加换行。Println 接受可变数量的参数,每个参数都是一个空接口。它返回打印的字符数量以及发生的任何错误。由于参数类型是空接口,我们可以传递任何数据类型。我们可以传递字符串、整数、浮点数、结构体或任何其他数据类型。Println 函数的每个参数都根据该参数类型的默认格式说明符进行格式化。例如,结构体将根据下面的说明符进行格式化。

%v

此格式说明符仅打印结构体中的值部分。让我们看一个例子。

程序

package main
import "fmt"
type employee struct {
    Name string
    Age  int
}
func main() {
    name := "John"
    age := 21
    fmt.Println("Name is: ", name)
    fmt.Println("Age is: ", age)
    e := employee{
        Name: name,
        Age:  age,
    }
    fmt.Println(e)
    fmt.Println("a", 12, "b", 12.0)

    bytesPrinted, err := fmt.Println("Name is: ", name)
    if err != nil {
	log.Fatalln("Error occured", err)
    }
    fmt.Println(bytesPrinted)
}

输出

Name is: John
Age is: 21
{John 21}
a 12 b 12
Name is: John
14

关于 Println 函数的一些重要事项

  • 它在末尾添加换行。这就是为什么每个输出都在不同的行上。

  • 输出中的每个参数将用空格分隔。这就是为什么

fmt.Println("Name is: ", name)

打印

Name is: John

两个参数之间会自动引入空格。

  • 它返回打印的字符数量或发生的任何错误。
bytesPrinted, err := fmt.Println("Name is: ", name)
if err != nil {
    log.Fatalln("Error occured", err)
}
fmt.Println(bytesPrinted)

将输出如下

Name is: John
14

bytesPrinted 的数量为 14,因为输出了 14 个字符。

另外,查看我们的 Golang 高级教程系列 – Golang 高级教程*

理解 Go 语言中的 Rune

来源:golangbyexample.com/understanding-rune-in-golang/

目录

概述

+   什么是 Unicode

+   UTF-8
  • 何时使用

  • 代码:

  • Rune 数组转字符串及反向操作

    • Rune 数组转字符串

    • 字符串转 Rune 数组

概述

在 Go 语言中,rune 是 int32 的别名,这意味着它是一个整数值。这个整数值用于表示一个 Unicode 代码点。要理解 rune,你必须知道什么是 Unicode。下面是简短的描述,但你可以参考关于它的著名博客文章 –

每个软件开发者绝对、一定要知道的关于 Unicode 和字符集的最低要求(没有借口!)

什么是 Unicode

Unicode 是 ASCII 字符的超集,它为每个存在的字符分配一个唯一的编号。这个唯一的编号称为 Unicode 代码点。例如

  • 数字 0 表示为 Unicode 点 U+0030 (十进制值 – 48)

  • 小写字母 b 的 Unicode 点表示为 U+0062 (十进制值 – 98)

  • 英镑符号 £ 表示为 Unicode 点 U+00A3 (十进制值 – 163)

访问 en.wikipedia.org/wiki/List_of_Unicode_characters 了解其他字符的 Unicode 点。但 Unicode 不会讨论这些代码点如何在内存中保存。这就是 utf-8 的用武之地。

UTF-8

utf-8 将每个 Unicode 点保存为 1、2、3 或 4 字节。ASCII 点使用 1 字节存储。这就是为什么 rune 是 int32 的别名,因为 Unicode 点在 Go 中最多可以为 4 字节,源代码在 Go 中是使用 utf-8 编码的,因此每个字符串也编码为 utf-8。

每个 rune 旨在引用一个 Unicode 点。例如,如果你在类型转换为 rune 数组后打印字符串,它将打印每个字符的 Unicode 点。对于下面的字符串 “0b£”,输出将为 – [U+0030 U+0062 U+00A3]

fmt.Printf("%U\n", []rune("0b£"))

何时使用

  • 当你打算在 rune 值中保存 Unicode 代码点时,应使用 rune。当数组中的所有值都意味着一个 Unicode 代码点时,应使用 rune 数组。

  • Rune 也用于表示一个字符。

声明 Rune

一个 rune 使用单引号中的字符声明,如下所示,声明一个名为 ‘rPound’ 的变量。

rPound := '£'

声明 Rune 后,你还可以执行以下操作

  • 打印类型 – 输出将为 int32
fmt.Printf("Type: %s\n", reflect.TypeOf(rPound))
  • 打印 Unicode 代码点 – 输出将为 U+00A3
fmt.Printf("Unicode CodePoint: %U\n", rPound)
  • 打印字符 – 输出将为 £
fmt.Printf("Character: %c\n", r)

代码:

下面是演示我们讨论的每个要点的代码

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    r := 'a'

    //Print Size
    fmt.Printf("Size: %d\n", unsafe.Sizeof(r))

    //Print Type
    fmt.Printf("Type: %s\n", reflect.TypeOf(r))

    //Print Code Point
    fmt.Printf("Unicode CodePoint: %U\n", r)

    //Print Character
    fmt.Printf("Character: %c\n", r)
    s := "0b£"

    //This will print the Unicode Points
    fmt.Printf("%U\n", []rune(s))

    //This will the decimal value of Unicode Code Point
    fmt.Println([]rune(s))
}

输出:

Size: 4
Type: int32
Unicode CodePoint: U+0061
Character: a
[U+0030 U+0062 U+00A3]
[48 98 163]

符文数组转换为字符串及其反向操作

符文数组转为字符串

package main

import "fmt"

func main() {
    runeArray := []rune{'a', 'b', '£'}
    s := string(runeArray)
    fmt.Println(s)
}

输出:

ab£

字符串转为符文数组

package main

import "fmt"

func main() {
    s := "ab£"
    r := []rune(s)
    fmt.Printf("%U\n", r)
}

输出:

[U+0061 U+0062 U+00A3]

理解 Set-Cookie 响应头

来源:golangbyexample.com/set-cookie-response-header/

目录

Set-Cookie 响应头概述

  • 示例

  • 程序

Set-Cookie 响应头概述

网页服务器可以将 Set-Cookie 头返回给客户端、浏览器或任何其他用户代理。然后,客户端应在其端存储此 cookie。该客户端将在每次请求时将此 cookie 发送给服务器。

您可以在这里阅读有关 HTTP cookie 的一般信息 – en.wikipedia.org/wiki/HTTP_cookie

下面是 Set-Cookie 头的语法

Set-Cookie: <cookie_name>=<cookie_value>; Expires={some_date}; Max-Age={some_integer}; Domain={some_domain}; Path={some_path}; 
SameSite={Strict/Lax/None}</cookie_value></cookie_name>

以下是 Set-Cookie 头的字段。这些字段通过 ‘;’ 连接以创建最终的 Set-Cookie 头值。

  • name=value – 这是表示 的名称-值对。名称和值由 ‘=’ 分隔。该字段是 cookie 的必填字段。所有其他字段为可选字段。

  • Expires={some_date} – 这指定了 cookie 的最大生命周期。它采用日期格式,并且 cookie 会在该日期后过期。

  • Max-Age={some_integer} – 它表示 cookie 过期的秒数。如果同时指定了 Expires 和 Max-Age,Max-Age 优先。

  • Domain={some_domain} – 它指定请求将发送到的域。

  • Path={some_path} – 客户端发送 cookies 的请求 URI 中存在的路径。如果路径与请求的 URI 不匹配,客户端将不发送 cookie。更高层次的路径匹配较低层次的路径。因此,/ 将匹配所有路径。而 /employee 将匹配 /employee、/employee/name、/employee/details。

  • Secure – 此标志意味着 cookie 仅在发出 HTTPS 请求时发送到服务器。

  • HttpOnly – 启用此标志后,javascript 将无法访问 cookie。这是为了防止 CSRF 攻击。

  • SameSite=StrictLaxNone – 此选项控制在浏览器进行跨域调用时,是否可以发送 cookies。

如上所述,这些字段可以使用 ‘;’ 组合以创建最终的 Set-Cookie 头。这里需要注意的重要事项是,客户端仅会在后续调用服务器时将名称-值对发送回服务器。所有其他选项仅供客户端使用。另一个需要注意的重要点是,服务器也可以在响应中发送多个 Set-Cookie 头。所有 Set-Cookie 响应头中的名称-值对将在后续调用中发送给服务器。

示例

这里有一些 Set-Cookie 头的示例

  • 仅名称-值对
show_pop=true
  • 带有 Expires 字段的名称-值对
show_pop=true; Expires=Tues, 27 Nov 2016 07:45:00 GMT
  • 带有其他字段的名称-值对
show_pop=true; Expires=Tues, 27 Nov 2016 07:45:00 GMT; Domain=foo_test.com; SameSite=Strict

程序

让我们看看 Set-Cookie 头的实际效果。我们将看到 Golang 中的示例。为此,首先创建一个监听在 8080 端口的 Golang 服务器。创建两个 API。

  • localhost:8080/doc – 在这个 API 中,服务器将在响应中设置 Set-Cookie 头。我们将从浏览器发起这个调用。浏览器将在其端保存这个 cookie。然后,浏览器将在对 localhost:8080 的任何其他请求中将相同的 cookie 发送回服务器。

  • localhost:8080/doc/id – 这是示例 API,演示浏览器实际上在请求中发送的同一 cookie,它在 Set-Cookie 头中收到的响应中。

首先,我们创建一个服务器。

go.mod

module sample.com/learn

go 1.16

main.go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	docHandler := http.HandlerFunc(docHandler)
	http.Handle("/doc", docHandler)

	docGetID := http.HandlerFunc(docGetID)
	http.Handle("/doc/id", docGetID)

	http.ListenAndServe(":8080", nil)
}

func docHandler(w http.ResponseWriter, r *http.Request) {
	cookie := &http.Cookie{
		Name:   "id",
		Value:  "abcd",
		MaxAge: 300,
	}
	http.SetCookie(w, cookie)
	w.WriteHeader(200)
	w.Write([]byte("Doc Get Successful"))
	return
}

func docGetID(w http.ResponseWriter, r *http.Request) {
	c, _ := r.Cookie("id")
	fmt.Println(c)
	w.WriteHeader(200)
	w.Write([]byte("Doc Get ID Successful"))
	return
}

从上面的代码可以看到我们有两个 API,如上所述。使用以下命令运行上述程序。

go run main.go

服务器将在 8080 端口启动。

现在从浏览器发起 API 调用 localhost:8080/doc。服务器在响应中发送以下 Set-Cookie

Set-Cookie: id=abcd; Max-Age=300

这在 API 调用的响应头中也是可见的。请看下面的屏幕截图。

![cookie_set_request](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/5743ff6b7299452f20052240e39f56a7.png)

现在,从不同的标签页发起另一个 API 调用。注意,在响应中发送回的是相同的 cookie。同时注意,正如我们在文章中提到的,只发送名称-值对。

![cookie_send_request](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/a3c457587ceebc81af0674c0439a1889.png)

查看我们全面的 Golang 教程系列 – Golang 综合教程*

理解 Go(Golang)中的时间和日期 – 完整指南

来源:golangbyexample.com/all-about-time-and-date-golang/

注意: 如果你有兴趣学习 Golang,那么我们有一个全面的 golang 教程系列,欢迎查看 – Golang 综合教程系列。现在让我们看看当前的教程。以下是内容目录。

目录

  • 概述

  • 结构

  • 创建一个新的时间

    • 使用 time.Now()")

    • 使用 time.Date()")

  • 理解持续时间

  • 加或减时间

    • 加到时间

    • 从时间中减去

  • 时间解析/格式化

    • 时间解析示例

    • 时间格式化示例

  • 时间差

  • 时间转换

    • 在不同的时区之间转换时间

概述

时间日期 在 Go 中使用 time.Time 结构表示。时间也可以表示为一个

  • Unix 时间(也称为 Epoch 时间) – 自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数。这段时间也被称为 Unix 纪元。

结构

time.Time 对象用于表示特定的时间点。 time.Time 结构如下

type Time struct {
    // wall and ext encode the wall time seconds, wall time nanoseconds,
    // and optional monotonic clock reading in nanoseconds.
    wall uint64
    ext  int64
    //Location to represent timeZone
    // The nil location means UTC
    loc *Location
}

如你所见,每个 time.Time 对象都有一个相关的 location 值,用于确定对应于该时间的分钟、小时、月份、日期和年份。

创建一个新的时间

使用 time.Now()

此函数可用于获取当前本地时间戳。函数的签名为

func Now() Time

使用 time.Date()

此函数返回对应于给定位置的 yyyy-mm-dd hh:mm:ss + nsec 纳秒的时间,并具有适当的时区。函数的签名为:

func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time

理解持续时间

持续时间 是两个时间瞬间之间经过的时间。它表示为 int64 纳秒 计数。因此在 Go 中,持续时间实际上是一个表示时间的纳秒数。如果持续时间的值等于 1000000000,则表示 1 秒1000 毫秒10000000000 纳秒

例如,两次时间值之间相差 1 小时的持续时间将等于下面的值,表示 1 小时中的纳秒数。

1 *60*60*1000*1000*1000

它在 time 包中表示如下。

type Duration int64

以下是一些在time包中定义的常见持续时间

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

一些定义在time.Time对象上的函数返回持续时间

  • func (t Time) Sub(u Time) Duration – 它返回持续时间 t-u

  • func Since(t Time) Duration – 它返回自 t 以来经过的持续时间

  • func Until(t Time) Duration – 它返回直到 t 的持续时间

加或减时间

现在你已经理解了持续时间,让我们看看如何对时间实例进行加减。

golang 中的time包定义了两种添加或减去时间的方法。

  • Add函数 - 用于将持续时间加到时间 t。由于持续时间可以表示为小时、分钟、秒、毫秒、微秒和纳秒,因此 Add 函数可以用于从时间中加/减小时、分钟、秒、毫秒、微秒和纳秒。其签名为
func (t Time) Add(d Duration) Time
  • AddDate函数 - 用于给时间 t 加/减年份、月份和天数。其签名为
func (t Time) AddDate(years int, months int, days int) Time

注意:正值用于加时间,负值用于减去。让我们看看加减时间的一个实际例子。

加到时间

以下代码可用于加时间

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()

    //Add 1 hours
    newT := t.Add(time.Hour * 1)
    fmt.Printf("Adding 1 hour\n: %s\n", newT)

    //Add 15 min
    newT = t.Add(time.Minute * 15)
    fmt.Printf("Adding 15 minute\n: %s\n", newT)

    //Add 10 sec
    newT = t.Add(time.Second * 10)
    fmt.Printf("Adding 10 sec\n: %s\n", newT)

    //Add 100 millisecond
    newT = t.Add(time.Millisecond * 10)
    fmt.Printf("Adding 100 millisecond\n: %s\n", newT)

    //Add 1000 microsecond
    newT = t.Add(time.Millisecond * 10)
    fmt.Printf("Adding 1000 microsecond\n: %s\n", newT)

    //Add 10000 nanosecond
    newT = t.Add(time.Nanosecond * 10000)
    fmt.Printf("Adding 1000 nanosecond\n: %s\n", newT)

    //Add 1 year 2 month 4 day
    newT = t.AddDate(1, 2, 4)
    fmt.Printf("Adding 1 year 2 month 4 day\n: %s\n", newT)
}

输出:

Adding 1 hour:
 2020-02-01 02:16:35.893847 +0530 IST m=+3600.000239893

Adding 15 minute:
 2020-02-01 01:31:35.893847 +0530 IST m=+900.000239893

Adding 10 sec:
 2020-02-01 01:16:45.893847 +0530 IST m=+10.000239893

Adding 100 millisecond:
 2020-02-01 01:16:35.903847 +0530 IST m=+0.010239893

Adding 1000 microsecond:
 2020-02-01 01:16:35.903847 +0530 IST m=+0.010239893

Adding 1000 nanosecond:
 2020-02-01 01:16:35.893857 +0530 IST m=+0.000249893

Adding 1 year 2 month 4 day:
 2021-04-05 01:16:35.893847 +0530 IST

减去时间

以下代码可用于减去时间

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()

    //Add 1 hours
    newT := t.Add(-time.Hour * 1)
    fmt.Printf("Subtracting 1 hour:\n %s\n", newT)

    //Add 15 min
    newT = t.Add(-time.Minute * 15)
    fmt.Printf("Subtracting 15 minute:\n %s\n", newT)

    //Add 10 sec
    newT = t.Add(-time.Second * 10)
    fmt.Printf("Subtracting 10 sec:\n %s\n", newT)

    //Add 100 millisecond
    newT = t.Add(-time.Millisecond * 10)
    fmt.Printf("Subtracting 100 millisecond:\n %s\n", newT)

    //Add 1000 microsecond
    newT = t.Add(-time.Millisecond * 10)
    fmt.Printf("Subtracting 1000 microsecond:\n %s\n", newT)

    //Add 10000 nanosecond
    newT = t.Add(-time.Nanosecond * 10000)
    fmt.Printf("Subtracting 1000 nanosecond:\n %s\n", newT)

    //Add 1 year 2 month 4 day
    newT = t.AddDate(-1, -2, -4)
    fmt.Printf("Subtracting 1 year 2 month 4 day:\n %s\n", newT)
}

输出:

Subtracting 1 hour:
 2020-02-01 00:18:29.772673 +0530 IST m=-3599.999784391

Subtracting 15 minute:
 2020-02-01 01:03:29.772673 +0530 IST m=-899.999784391

Subtracting 10 sec:
 2020-02-01 01:18:19.772673 +0530 IST m=-9.999784391

Subtracting 100 millisecond:
 2020-02-01 01:18:29.762673 +0530 IST m=-0.009784391

Subtracting 1000 microsecond:
 2020-02-01 01:18:29.762673 +0530 IST m=-0.009784391

Subtracting 1000 nanosecond:
 2020-02-01 01:18:29.772663 +0530 IST m=+0.000205609

Subtracting 1 year 2 month 4 day:
 2018-11-27 01:18:29.772673 +0530 IST

时间解析/格式化

如果你曾在其他语言中处理过时间/日期格式化/解析,你可能会注意到其他语言使用特殊的占位符进行时间/日期格式化。例如,ruby 语言使用

  • %d 表示天

  • %Y 表示年份

等等

Golang 没有使用上述代码,而是使用看起来像日期和时间的日期和时间格式占位符。Go 使用标准时间,即:

Mon Jan 2 15:04:05 MST 2006  (MST is GMT-0700)
or 
01/02 03:04:05PM '06 -0700

所以如果你注意到,go 使用

  • 01 表示月份的天数,

  • 02 表示月份

  • 03 表示小时,

  • 04 表示分钟

  • 05 代表秒

  • 等等

以下占位符表描述了确切的映射。Go 采用了一种更务实的方法,你不需要像其他语言一样记住或查找传统的格式化代码。

类型 占位符
202_2
星期几 MondayMon
011JanJanuary
200606
小时 03315
分钟 044
055
毫秒 (ms) .000 //尾随零将被包括或 .999 //尾随零将被省略
微秒 (μs) .000000 //尾随零将被包括或 .999999 //尾随零将被省略
纳秒 (ns) .000000000 //尾随零将被包括或 .999999999 //尾随零将被省略
上午/下午 PMpm
时区 MST
时区偏移 Z0700Z070000Z07Z07:00Z07:00:00-0700-070000-07-07:00-07:00:00

时间解析示例

现在回到time.Parse。该函数的签名为

func Parse(layout, value string) (Time, error)

time.Parse函数接受两个参数。

  • 第一个参数是由时间格式占位符组成的布局。

  • 第二个参数是表示时间的实际格式化字符串。

你需要确保布局字符串(第一个参数)与要解析的时间的字符串表示(第二个参数)相匹配。

  • 对于解析2020-01-29,布局字符串应为06-01-022006-01-02,或其他基于上述占位符表正确映射的内容。

  • 同样,对于解析“2020-Jan-29 星期三 12:19:25”,布局字符串可以是“2006-Jan-02 星期一 03:04:05”

下面是time.Parse()的工作代码示例。

package main

import (
    "fmt"
    "time"
)

func main() {
    //Parse YYYY-MM-DD
    timeT, _ := time.Parse("2006-01-02", "2020-01-29")
    fmt.Println(timeT)

    //Parse YY-MM-DD
    timeT, _ = time.Parse("06-01-02", "20-01-29")
    fmt.Println(timeT)

    //Parse YYYY-#{MonthName}-DD
    timeT, _ = time.Parse("2006-Jan-02", "2020-Jan-29")
    fmt.Println(timeT)

    //Parse YYYY-#{MonthName}-DD WeekDay HH:MM:SS
    timeT, _ = time.Parse("2006-Jan-02 Monday 03:04:05", "2020-Jan-29 Wednesday 12:19:25")
    fmt.Println(timeT)

    //Parse YYYY-#{MonthName}-DD WeekDay HH:MM:SS PM Timezone TimezoneOffset
    timeT, _ = time.Parse("2006-Jan-02 Monday 03:04:05 PM MST -07:00", "2020-Jan-29 Wednesday 12:19:25 AM IST +05:30")
    fmt.Println(timeT)
}

输出:

2020-01-29 00:00:00 +0000 UTC
2020-01-29 00:00:00 +0000 UTC
2020-01-29 00:00:00 +0000 UTC
2020-01-29 12:19:25 +0000 UTC
2020-01-29 00:19:25 +0530 IST

时间格式示例

time.Format函数可以用来将时间格式化为字符串表示。该函数的签名是。

func (t Time) Format(layout string)

让我们看看一些时间格式代码示例。

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    //Format YYYY-MM-DD
    fmt.Printf("YYYY-MM-DD: %s\n", now.Format("2006-01-02"))

    //Format YY-MM-DD
    fmt.Printf("YY-MM-DD: %s\n", now.Format("06-01-02"))

    //Format YYYY-#{MonthName}-DD
    fmt.Printf("YYYY-#{MonthName}-DD: %s\n", now.Format("2006-Jan-02"))

    //Format HH:MM:SS
    fmt.Printf("HH:MM:SS: %s\n", now.Format("03:04:05"))

    //Format HH:MM:SS Millisecond
    fmt.Printf("HH:MM:SS Millisecond: %s\n", now.Format("03:04:05 .999"))

    //Format YYYY-#{MonthName}-DD WeekDay HH:MM:SS
    fmt.Printf("YYYY-#{MonthName}-DD WeekDay HH:MM:SS: %s\n", now.Format("2006-Jan-02 Monday 03:04:05"))

    //Format YYYY-#{MonthName}-DD WeekDay HH:MM:SS PM Timezone TimezoneOffset
    fmt.Printf("YYYY-#{MonthName}-DD WeekDay HH:MM:SS PM Timezone TimezoneOffset: %s\n", now.Format("2006-Jan-02 Monday 03:04:05 PM MST -07:00"))
}

输出:

YYYY-MM-DD: 2020-01-25
YY-MM-DD: 20-01-25
YYYY-#{MonthName}-DD: 2020-Jan-25
HH:MM:SS: 11:14:16
HH:MM:SS Millisecond: 11:14:16 .213
YYYY-#{MonthName}-DD WeekDay HH:MM:SS: 2020-Jan-25 Saturday 11:14:16
YYYY-#{MonthName}-DD WeekDay HH:MM:SS PM Timezone TimezoneOffset: 2020-Jan-25 Saturday 11:14:16 PM IST +05:30

时间差

time包有一个方法Sub,可以用来获取两个不同时间值之间的差。该函数的签名是。

func (t Time) Sub(u Time) Duration
currentTime := time.Now()
oldTime := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
diff := currentTime.Sub(oldTime)

时间转换

下面的代码展示了转换。

  • time.Time 转换为 Unix 时间戳。

  • Unix 时间戳转换为 time.Time。

package main

import (
    "fmt"
    "time"
)

func main() {
    tNow := time.Now()

    //time.Time to Unix Timestamp
    tUnix := tNow.Unix()
    fmt.Printf("timeUnix %d\n", tUnix)

    //Unix Timestamp to time.Time
    timeT := time.Unix(tUnix, 0)
    fmt.Printf("time.Time: %s\n", timeT)
}

输出:

timeUnix 1257894000
time.Time: 2009-11-10 23:00:00 +0000 UTC

在不同时间区域之间转换时间

In函数可以用来更改与特定time.Time对象相关联的location。每当在任何time.Time对象(例如 t)上调用In函数时,

  • 创建一个t的副本,表示相同的时间瞬间。

  • t 的位置被设置为传递给 In 函数的地点,以便于显示。

  • t被返回。

让我们看看下面的工作代码,这可以用来更改与特定时间相关联的位置值。

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    loc, _ := time.LoadLocation("UTC")
    fmt.Printf("UTC Time: %s\n", now.In(loc))

    loc, _ = time.LoadLocation("Europe/Berlin")
    fmt.Printf("Berlin Time: %s\n", now.In(loc))

    loc, _ = time.LoadLocation("America/New_York")
    fmt.Printf("New York Time: %s\n", now.In(loc))

    loc, _ = time.LoadLocation("Asia/Dubai")
    fmt.Printf("Dubai Time: %s\n", now.In(loc))
}

输出:

UTC Time: 2020-01-31 18:09:41.705858 +0000 UTC
Berlin Time: 2020-01-31 19:09:41.705858 +0100 CET
New York Time: 2020-01-31 13:09:41.705858 -0500 EST
Dubai Time: 2020-01-31 22:09:41.705858 +0400 +04

理解 Golang 中的 uintptr

来源:golangbyexample.com/understanding-uintptr-golang/

目录

  • 概述

  • 属性

  • 目的

概述

这是一种无符号整数类型,足够大以保存任何指针地址。因此它的大小是平台相关的。它只是一个地址的整数表示。

属性

  • uintptr可以转换为unsafe.Pointer,反之亦然。稍后我们将讨论uintptr转换为unsafe.Pointer的用途。

  • 可以对uintptr进行算术运算。请注意,Go 中的指针unsafe.Pointer不能进行算术运算。

  • uintptr虽然保存了一个指针地址,但只是一个值,并不引用任何对象。因此

    • 如果对应的对象移动,其值将不会更新。例如,当 goroutine 栈变化时。

    • 相应的对象可以被垃圾回收。GC 不会将uintptr视为活引用,因此它们可以被垃圾回收。

目的

uintptr可以用于以下目的:

  • uintptr 的一个目的是与 unsafe.Pointer 一起用于不安全的内存访问。

    • unsafe.Pointer 被转换为 uintptr。

    • 然后在 uintptr 上进行算术运算。

    • uintptr 被转换回 unsafe.Pointer 以访问现在由地址指向的对象。

注意,上述步骤应与垃圾回收器原子性相关,否则可能导致问题。例如,在第一步后,引用的对象可能会被回收。如果发生这种情况,则在第三步之后,指针将成为无效的 Go 指针,并可能导致程序崩溃。查看 unsafe 包文档。

golang.org/pkg/unsafe/#Pointer

它列出了上述转换何时可以安全。请参见下面提到的场景的代码。

在下面的代码中,我们进行如下运算,以获取结构体 sample 中字段“b”的地址,然后打印该地址处的值。下面的代码与垃圾回收器是原子的。

p := unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.b))
package main
import (
    "fmt"
    "unsafe"
)
type sample struct {
    a int
    b string
}
func main() {
    s := &sample{a: 1, b: "test"}

   //Getting the address of field b in struct s
    p := unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.b))

    //Typecasting it to a string pointer and printing the value of it
    fmt.Println(*(*string)(p))
}

输出:

test
  • uintptr 的另一个目的,是当你想保存指针地址值以进行打印或存储时。由于地址仅被存储而不引用任何东西,因此相应的对象可以被垃圾回收。

参见下面的代码,我们将 unsafe.Pointer 转换为 uintptr 并打印它。同时,注意如前所述,一旦 unsafe.Pointer 被转换为 uintptr,引用就丢失了,引用变量可以被垃圾回收。

package main

import (
    "fmt"
    "unsafe"
)

type sample struct {
    a int
    b string
}

func main() {
    s := &sample{
        a: 1,
        b: "test",
    }
    //Get the address as a uintptr
    startAddress := uintptr(unsafe.Pointer(s))
    fmt.Printf("Start Address of s: %d\n", startAddress)
}

输出:

输出将依赖于机器,因为它是一个地址。

Start Address of s: 824634330992

理解 Go 中的var关键字(Golang)

来源:golangbyexample.com/understanding-var-keyword-go/

var关键字是 Go 语言中的保留关键字,用于声明变量。变量使用var关键字声明,但还有其他方式可以声明变量,例如使用:=运算符。

以下是使用var关键字声明变量的不同方式。

无初始值的单个变量声明

以下是单个变量声明的格式,不包含初始值。首先是var关键字,其次是变量名称,最后是变量类型。此外,请注意当未提供值时,变量会被初始化为该类型的默认值,这也被称为该类型的零值。在 Go 中,int的默认值或零值是零。

var <variable_name></variable_name> 

请看下面的示例,它声明了一个名为aaa的类型为int的变量。

package main

import "fmt"

func main() {
    var aaa int
    fmt.Println(aaa)
}

输出: 它将打印int的默认值,即零。

0

带初始值的单个变量声明

以下是带有初始值的单个变量声明格式。与上述相同,唯一不同的是我们在最后给变量赋值。

var <variable_name> <type> = <value></value></type></variable_name>

请看下面的示例,它声明了一个名为aaa的类型为int的变量,并给它赋值为8

package main

import "fmt"

func main() {
    var aaa int = 8
    fmt.Println(aaa)
}

输出:

8

无初始值的多个变量声明

以下是多个变量声明的格式,没有赋初值。请注意,只有相同类型的变量可以一起声明。类型在最后出现。

var <name1><name2>etc</name2></name1> 

请看下面的示例。

package main

import "fmt"

func main() {
    var aaa, bbb int
    fmt.Println(aaa)
    fmt.Println(bbb)
}

输出: 它将打印aaabbb的默认或零值,均为零。

0
0

带有初始值的多个变量声明

以下是带有初始值的多个变量声明的格式。请注意,只有相同类型的变量可以一起声明。类型在最后出现。

var <name1> <name2> etc <type> = <value1> <value2> etc</value2></value1></type></name2></name1>

请看下面的代码示例。变量aaabbb在一次声明中分别赋值为 8 和 9。

package main

import "fmt"

func main() {
    var aaa, bbb int = 8, 9
    fmt.Println(aaa)
    fmt.Println(bbb)
}

输出:

8
9

声明不同类型的变量

以下是声明多个不同类型变量的格式。此时可以或不可以为变量赋值。未赋值的变量将获得该类型的默认值。在下面的示例中,我们看到三个声明。

package main

import "fmt"

func main() {
    var (
        aaa int
        bbb int    = 8
        ccc string = "a"
    )

    fmt.Println(aaa)
    fmt.Println(bbb)
    fmt.Println(ccc)
}

输出:

0
8
a

无类型或类型推断的变量声明

变量也可以在不指定类型的情况下声明。

Go 编译器会根据赋给它的值推断类型。因此,如果变量有初始值,则类型可以省略。这也被称为类型推断。以下是这种声明的格式。

var <varialbe_name> = value</varialbe_name>

以下是基本类型的类型推断表:intfloat复数字符串布尔值字符。这基本上意味着如果值是整数,则推断出的变量类型将是int;如果值是浮点数,则推断出的变量类型将是float64,依此类推,具体如下表。

整数 int
浮点数 float64
复数 complex128
字符串 string
布尔值 bool
字符 int32 或 rune

对于其他类型,如数组指针结构体等,类型推断将基于值进行。让我们看看上述点的一个工作示例。注意t的类型被正确推断为 int,因为赋给它的值是 123,这是 int。同样,u的类型也被正确推断为 string,因为赋给它的值是一个字符串

还要注意,变量z的类型被正确推断为结构体a

package main

import "fmt"

func main() {
    var t = 123      //Type Inferred will be int
    var u = "circle" //Type Inferred will be string
    var v = 5.6      //Type Inferred will be float64
    var w = true     //Type Inferred will be bool
    var x = 'a'      //Type Inferred will be rune
    var y = 3 + 5i   //Type Inferred will be complex128
    var z = &a{name: "test"}

    fmt.Printf("Type: %T Value: %v\n", t, t)
    fmt.Printf("Type: %T Value: %v\n", u, u)
    fmt.Printf("Type: %T Value: %v\n", v, v)
    fmt.Printf("Type: %T Value: %v\n", w, w)
    fmt.Printf("Type: %T Value: %v\n", x, x)
    fmt.Printf("Type: %T Value: %v\n", y, y)
    fmt.Printf("Type: %T Value: %v\n", z, z)
}

type a struct {
    name string
}

输出:

Type: int Value: 123
Type: string Value: circle
Type: float64 Value: 5.6
Type: bool Value: true
Type: int32 Value: 97
Type: complex128 Value: (3+5i)
Type: *main.a Value: &{test}

在 Go 中理解 WaitGroup (Golang)

来源:golangbyexample.com/understanding-waitgroup-go/

WaitGroup 是 Go 的 sync 包中的一个结构体。它通常用于等待一组 goroutine 完成执行。WaitGroup 具有以下方法:

  • Add 方法用于设置要等待的 goroutine 数量。

  • Done 方法由每个 goroutine 在完成时调用。

  • Wait 方法用于阻塞,直到所有 goroutine 完成并调用了 Done 方法。

让我们来看一个工作的代码:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go sleep(&wg, time.Second*1)
    go sleep(&wg, time.Second*2)
    wg.Wait()
    fmt.Println("All goroutines finished")
}

func sleep(wg *sync.WaitGroup, t time.Duration) {
    defer wg.Done()
    time.Sleep(t)
    fmt.Println("Finished Execution")
}

输出:

Finished Execution
Finished Execution
All goroutines finished

Golang 中的唯一路径程序

来源:golangbyexample.com/unique-paths-program-golang/

目录

  • 概述**

  • 第一种变体

    • 程序

    • 程序

概述

有一个 m*n 的网格。在位置(0,0)有一个机器人。机器人只能向右和向下移动。机器人到达右下角即(m-1, n-1)的总方式数量是多少?

示例

Input: m=2 , n=2
Output: 2

Robot can reach the right down corner in two ways. 
1\. [0,0] -> [0,1]-> [1, 1]
2\. [0,0] -> [1,0]-> [1, 1]

这个程序还有另一种变体,其中网格中的一个项可能包含障碍物。我们先看看第一种变体,稍后再看第二种变体。

第一种变体

我们将通过动态编程解决这个问题。

  • 创建一个大小为 m*n 的路径矩阵。

  • paths[i][j]表示机器人到达(i,j)索引的方式数量。

  • paths[0][0] = 0。

  • paths[i][j] = paths[i-1][j] + paths[i][j-1]

程序

这是相同程序的代码。

package main

import "fmt"

func uniquePaths(m int, n int) int {
	paths := make([][]int, m)

	for i := 0; i < m; i++ {
		paths[i] = make([]int, n)
	}

	paths[0][0] = 1

	for i := 1; i < m; i++ {
		paths[i][0] = 1
	}

	for i := 1; i < n; i++ {
		paths[0][i] = 1
	}

	for i := 1; i < m; i++ {
		for j := 1; j < n; j++ {
			paths[i][j] = paths[i-1][j] + paths[i][j-1]
		}
	}

	return paths[m-1][n-1]
}

func main() {
	output := uniquePaths(3, 7)
	fmt.Println(output)
}

输出

6

第二种变体我们将通过动态编程解决这个问题。

  • 创建一个大小为 m*n 的路径矩阵。

  • paths[i][j]表示机器人到达(i,j)索引的方式数量。

  • paths[0][0] = 0。

  • 如果paths[i][j]不是障碍物,则paths[i][j] = paths[i-1][j] + paths[i][j-1]

  • 如果paths[i][j]是障碍物,则paths[i][j] = 0。

程序

package main

import "fmt"

func uniquePathsWithObstacles(obstacleGrid [][]int) int {

	m := len(obstacleGrid)
	n := len(obstacleGrid[0])
	paths := make([][]int, len(obstacleGrid))

	for i := 0; i < m; i++ {
		paths[i] = make([]int, n)
	}

	if obstacleGrid[0][0] != 1 {
		paths[0][0] = 1
	}

	for i := 1; i < m; i++ {
		if obstacleGrid[i][0] == 1 {
			break
		} else {
			paths[i][0] = paths[i-1][0]
		}

	}

	for i := 1; i < n; i++ {
		if obstacleGrid[0][i] == 1 {
			break
		} else {
			paths[0][i] = paths[0][i-1]
		}

	}

	for i := 1; i < m; i++ {
		for j := 1; j < n; j++ {
			if obstacleGrid[i][j] != 1 {
				paths[i][j] = paths[i-1][j] + paths[i][j-1]
			}

		}
	}

	return paths[m-1][n-1]
}

func main() {
	output := uniquePathsWithObstacles([][]int{{0, 0, 0}, {0, 1, 0}, {0, 0, 0}})
	fmt.Println(output)
}

输出

2

注意: 请查看我们的 Golang 高级教程。本系列的教程内容详尽,我们尝试用示例覆盖所有概念。本教程适合那些希望获得专业知识并深入理解 Golang 的读者 - Golang 高级教程

如果你有兴趣了解所有设计模式如何在 Golang 中实现。如果是的话,这篇文章适合你 - 所有设计模式 Golang

更新 Go (Golang) 中映射中的键

来源:golangbyexample.com/update-key-map-golang/

当尝试向映射中添加一个已经存在的键时,新值会覆盖旧值。这类似于在映射中更新键。

让我们看看一个例子

package main

import "fmt"

func main() {
    //Declare
    employeeSalary := make(map[string]int)

    //Adding a key value
    fmt.Println("Before update")
    employeeSalary["Tom"] = 2000
    fmt.Println(employeeSalary)

    fmt.Println("After update")
    employeeSalary["Tom"] = 3000
    fmt.Println(employeeSalary)
}

输出

Before update
map[Tom:2000]
After update
map[Tom:3000]

在上面的程序中,当用新值“3000”写入相同的键时,它会覆盖现有值“2000”。当我们再次打印映射时,打印的值为 3000。

Go(Golang)中的 defer 函数使用案例

来源:golangbyexample.com/defer-use-case-go/

目录

  • 概述

  • 示例

概述

如名称所示,Defer 用于推迟函数中的清理活动。这些清理活动将在函数结束时执行。这些清理活动将在一个由 defer 调用的不同函数中完成。这个不同的函数在其所在函数返回之前的最后执行。以下是 defer 函数的语法。

defer {function_or_method_call}

理解使用场景 defer 函数的一个好例子是查看写入文件的使用场景。打开以进行写入的文件也必须关闭。

示例

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    err := writeToTempFile("Some text")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Write to file succesful")
}

func writeToTempFile(text string) error {
    file, err := os.Open("temp.txt")
    if err != nil {
        return err
    }
    n, err := file.WriteString("Some text")
    if err != nil {
        return err
    }
    fmt.Printf("Number of bytes written: %d", n)
    file.Close()
    return nil
}

在上述程序中,在writeToTempFile函数中,我们打开一个文件,然后尝试向该文件写入一些内容。写入完内容后,我们关闭文件。在写入操作期间可能会出现错误,从而导致函数在未关闭文件的情况下返回。Defer函数可以帮助避免这些问题。Defer函数总是在其所在函数返回之前执行。让我们在这里重写上述程序并使用defer函数。

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    err := writeToTempFile("Some text")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Write to file succesful")
}

func writeToTempFile(text string) error {
    file, err := os.Open("temp.txt")
    if err != nil {
        return err
    }
    defer file.Close()

    n, err := file.WriteString("Some text")
    if err != nil {
        return err
    }
    fmt.Printf("Number of bytes written: %d", n)
    return nil
}

在上述程序中,我们在打开文件后使用defer file.Close()。这将确保即使写入文件时出现错误,文件也会被关闭。Defer 函数确保无论函数中有多少个返回语句,文件都将被关闭。

Go (Golang)中的用户定义函数类型

来源:golangbyexample.com/user-defined-function-type-go/

目录

  • 概述

  • 代码

    • 示例 1

    • 示例 2

概述

在 GO 中,函数也是一种类型。如果两个函数是同一类型的话。

  • 它们具有相同数量的参数,每个参数都是相同类型的。

  • 它们具有相同数量的返回值,并且每个返回值都是相同类型的。

可以使用type关键字声明用户定义的类型作为函数,如下所示。area是类型为func(int, int) int的函数名称。

type area func(int, int) int

代码

示例 1

在这个例子中,我们创建一个用户定义的函数类型area。然后在主函数中创建一个类型为area的变量。

package main

import "fmt"

type area func(int, int) int

func main() {
    var areaF area = func(a, b int) int {
        return a * b
    }
    print(2, 3, areaF)
}

func print(x, y int, a area) {
    fmt.Printf("Area is: %d\n", a(x, y))
}

输出:

6

示例 2

在这个例子中,我们也创建一个用户定义的函数类型area。然后我们创建一个函数getAreaFunc(),它返回类型为area的函数。

package main

import "fmt"

type area func(int, int) int

func main() {
    areaF := getAreaFunc()
    print(2, 3, areaF)

}

func print(x, y int, a area) {
    fmt.Printf("Area is: %d\n", a(x, y))
}

func getAreaFunc() area {
    return func(x, y int) int {
        return x * y
    }
}

输出:

6

在 GO(Golang)中使用上下文包 – 完整指南

来源:golangbyexample.com/using-context-in-golang-complete-guide/

介绍:

定义:

上下文是 GO 提供的一个包。让我们首先了解一些已经存在的问题,以及上下文包尝试解决的问题。

问题陈述:

  • 假设你启动了一个函数,需要将一些公共参数传递给下游函数。你不能将这些公共参数作为每个参数传递给所有下游函数。

  • 你启动了一个 goroutine,进而又启动了更多的 goroutines。假设你正在进行的任务不再需要。那么,如何通知所有子 goroutines 优雅地退出,以便释放资源?

  • 一个任务应在指定的超时时间内完成,例如 2 秒。如果没有,它应优雅地退出或返回。

  • 一个任务应在截止时间内完成,例如它应在下午 5 点之前结束。如果未完成,则应优雅地退出并返回。

如果你注意到上述所有问题都适用于 HTTP 请求,但这些问题同样适用于许多不同的领域。

对于 web HTTP 请求,当客户端断开连接时,需要取消请求,或者请求必须在指定的超时时间内完成,同时请求范围的值如 request_id 需要对所有下游函数可用。

何时使用(一些用例):

  • 将数据传递给下游。例如,一个 HTTP 请求创建了一个 request_id,request_user,需要在所有下游函数中传递以进行分布式跟踪。

  • 当你想在中途停止操作时 – 由于客户端断开连接,HTTP 请求应被停止。

  • 当你希望在启动后的一段特定时间内停止操作,即超时 – 例如,HTTP 请求应在 2 秒内完成,否则应被终止。

  • 当你想在某个时间之前停止一个操作时 – 例如,如果没有完成,定时任务需要在 5 分钟内终止。

上下文接口

理解上下文的核心是了解 Context 接口

type Context interface {
    //It retures a channel when a context is cancelled, timesout (either when deadline is reached or timeout time has finished)
    Done() <-chan struct{}

    //Err will tell why this context was cancelled. A context is cancelled in three scenarios.
    // 1\. With explicit cancellation signal
    // 2\. Timeout is reached
    // 3\. Deadline is reached
    Err() error

    //Used for handling deallines and timeouts
    Deadline() (deadline time.Time, ok bool)

    //Used for passing request scope values
    Value(key interface{}) interface{}
}

创建新上下文

context.Background():

上下文包函数 Background() 返回一个空的 Context,它实现了 Context 接口。

  1. 它没有值

  2. 它从不被取消

  3. 它没有截止日期

那么,context.Background() 的作用是什么?context.Background() 作为所有上下文的根,将从中派生。随着我们深入了解,这将变得更加清晰。

context.ToDo():

  • context 包中的 ToDo 函数返回一个空的 Context。该上下文用于当周围函数没有传递上下文时,想要在当前函数中使用该上下文作为占位符,并计划在不久的将来添加实际上下文。将其作为占位符的一个用途是它有助于在静态代码分析工具中的验证。

  • 它也是一个与 context.Background()相同的空 Context。

上述两种方法描述了一种创建新上下文的方法。可以从这些上下文中派生更多上下文。这就是上下文树的出现。

上下文树

在理解上下文树之前,请确保在使用context时它在后台隐式创建。你将在 Go 的上下文包中找不到任何提及。

每当使用上下文时,从 context.Background()获取的空上下文是所有上下文的根。context.ToDo()也充当根上下文,但如上所述,它更像是未来使用的上下文占位符。这个空上下文没有任何功能,我们可以通过从中派生新上下文来添加功能。基本上,通过包装已存在的不可变上下文并添加附加信息来创建新的上下文。让我们看看创建的上下文树的一些示例。

两级树

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "msgId", "someMsgId")

在上面

  • rootCtx是没有功能的空 Context。

  • childCtx源自 rootCtx,并具有存储请求范围值的功能。在上面的示例中,它存储键值对{"msgId" : "someMsgId"}。

三级树

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "msgId", "someMsgId")
childOfChildCtx, cancelFunc := context.WithCancel(childCtx)

在上面

  • rootCtx是没有功能的空 Context。

  • childCtx源自 rootCtx,并具有存储请求范围值的功能。在上面的示例中,它存储键值对{"msgId" : "someMsgId"}。

  • childOfChildCtx源自 childCtx。它具有存储请求范围值的功能,并且还具有触发取消信号的功能。cancelFunc 可用于触发取消信号。

多级树

rootCtx := context.Background()
childCtx1 := context.WithValue(rootCtx, "msgId", "someMsgId")
childCtx2, cancelFunc := context.WithCancel(childCtx1)
childCtx3 := context.WithValue(rootCtx, "user_id", "some_user_id)

在上面:

  • rootCtx是没有功能的空 Context。

  • childCtx1源自rootCtx,并具有存储请求范围值的功能。在上面的示例中,它存储键值对{"msgId" : "someMsgId"}。

  • childCtx2源自childCtx1。它具有触发取消信号的功能。cancelFunc 可用于触发取消信号。

  • childCtx3源自rootCtx,具有存储当前用户的功能。

上述三级树如下所示。

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/5f51494c049ab64059c6f087ec5e4799.png)

由于它是树形结构,因此也可以为特定节点创建更多子节点。例如,我们可以从childCtx1派生一个新上下文childCtx4

childCtx4 := context.WithValue(childCtx1, "current_time", "some_time)

添加上述节点后的树如下所示:

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/3060e2e36d25c6a5579b4c7f756f3667.png)

目前,可能不清楚如何使用 WithValue()或 WithCancel()函数。现在只需理解,在使用上下文时,会创建一个以emptyCtx为根的上下文树。这些函数在我们继续时会更清晰

从上下文派生

派生上下文可以通过 4 种方式创建

  • 传递请求范围的值 - 使用WithValue()函数的上下文包

  • 使用取消信号 - 使用WithCancel()函数的上下文包

  • 使用截止日期 - 使用WithDeadine()函数的上下文包

  • 使用超时 - 使用WithTimeout()函数的上下文包

让我们详细理解上述每一点

context.WithValue()

用于传递请求范围的值。该函数的完整签名为

withValue(parent Context, key, val interface{}) (ctx Context)

它接受一个父上下文、键、值并返回一个派生上下文。这个派生上下文与关联的。这里的父上下文可以是 context.Background()或其他任何上下文。此外,任何从此上下文派生的上下文将具有此值。

#Root Context
ctxRoot := context.Background() - #Root context 

#Below ctxChild has acess to only one pair {"a":"x"}
ctxChild := context.WithValue(ctxRoot, "a", "x") 

#Below ctxChildofChild has access to both pairs {"a":"x", "b":"y"} as it is derived from ctxChild
ctxChildofChild := context.WithValue(ctxChild, "b", "y") 

示例:

withValue()的完整工作示例。在下面的示例中,我们为每个传入请求注入一个 msgId。如果你注意到下面的程序

  • injectMsgID 是一个网络 HTTP 中间件函数,在上下文中填充"msgID"字段

  • HelloWorld 是 API "localhost:8080/welcome"的处理函数,它从上下文中获取 msgID 并作为响应头发送回去

package main

import (
    "context"
    "net/http"
    "github.com/google/uuid"
)

func main() {
    helloWorldHandler := http.HandlerFunc(HelloWorld)
    http.Handle("/welcome", inejctMsgID(helloWorldHandler))
    http.ListenAndServe(":8080", nil)
}

//HelloWorld hellow world handler
func HelloWorld(w http.ResponseWriter, r *http.Request) {
    msgID := ""
    if m := r.Context().Value("msgId"); m != nil {
        if value, ok := m.(string); ok {
            msgID = value
        }
    }
    w.Header().Add("msgId", msgID)
    w.Write([]byte("Hello, world"))
}

func inejctMsgID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        msgID := uuid.New().String()
        ctx := context.WithValue(r.Context(), "msgId", msgID)
        req := r.WithContext(ctx)
        next.ServeHTTP(w, req)

    })
}

只需在运行上面的程序后对上述请求进行 curl 调用

curl -v http://localhost/welcome

这里将是响应。注意响应头中填充的MsgId。injectMsgId 函数作为中间件,向请求上下文注入唯一的 msgId。

curl -v http://localhost:8080/welcome
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /do HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Msgid: a03ff1d4-1464-42e5-a0a8-743c5af29837
< Date: Mon, 23 Dec 2019 16:51:01 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host localhost left intact

context.WithCancel()

用于取消信号。下面是WithCancel()函数的签名

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

context.WithCancel()函数返回两个东西

  • 具有新完成通道的父上下文的副本。

  • 一个取消函数,当调用时关闭这个完成通道

只有上下文的创建者应该调用取消函数。强烈不建议传递cancel函数。让我们通过一个示例来理解withCancel

示例:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    cancelCtx, cancelFunc := context.WithCancel(ctx)
    go task(cancelCtx)
    time.Sleep(time.Second * 3)
    cancelFunc()
    time.Sleep(time.Second * 1)
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

输出:

1
2
3
Gracefully exit
context canceled

在上面的程序中

  • 一旦调用cancelFunc,任务函数将优雅地退出。一旦调用 cancelFunc,上下文包将错误字符串设置为"context cancelled"。这就是ctx.Err()的输出为"context cancelled"的原因

context.WithTimeout()

用于基于时间的取消。该函数的签名为

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithTimeout()函数将

  • 将返回具有新完成通道的父上下文的副本。

  • 接受一个超时持续时间,在此之后这个完成通道将关闭,上下文将被取消

  • 一个可以在上下文需要在超时之前取消时调用的取消函数。

让我们看一个例子

示例:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
    defer cancel()
    go task1(cancelCtx)
    time.Sleep(time.Second * 4)
}

func task1(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

输出:

1
2
3
Gracefully exit
context deadline exceeded

在上面的程序中

  • 一旦超时 3 秒到期,任务函数将优雅退出。错误字符串由上下文包设置为“上下文截止日期已超过”。这就是 ctx.Err()输出为“上下文截止日期已超过”的原因。

context.WithDeadline()

用于基于截止日期的取消。该函数的签名为

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

context.WithDeadline()函数

  • 将返回一个带有新完成通道的父上下文副本。

  • 接受一个截止日期,截止日期后此完成通道将关闭并取消上下文。

  • 一个可以在上下文需要在截止日期之前被取消时调用的取消函数。

让我们看看一个例子。

示例:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))
    defer cancel()
    go task(cancelCtx)
    time.Sleep(time.Second * 6)
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

输出:

1
2
3
4
5
Gracefully exit
context deadline exceeded

在上面的程序中

  • 一旦超时 5 秒到期,任务函数将优雅退出,因为我们设定的截止日期为 Time.now() + 5 秒。错误字符串由上下文包设置为“上下文截止日期已超过”。这就是 ctx.Err()输出为“上下文截止日期已超过”的原因。

我们学到了什么

如何创建上下文:

  • 使用 context.Background()

  • 使用 context.Todo()

上下文树

派生新的上下文

  • context.WithValue()

  • context.WithCancel()

  • context.WithTimeout()

  • context.WithDeadline()

最佳实践与注意事项

以下是你在使用上下文时可以遵循的最佳实践列表。

  • 不要在结构体类型中存储上下文。

  • 上下文应该在你的程序中流动。例如,在 HTTP 请求的情况下,可以为每个传入请求创建一个新的上下文,该上下文可以用于保存 request_id 或在上下文中放入一些公共信息,如当前登录用户,这在特定请求中可能会很有用。

  • 始终将上下文作为函数的第一个参数传递。

  • 每当你不确定是否使用上下文时,最好使用 context.ToDo()作为占位符。

  • 只有父级 goroutine 或函数应该取消上下文。因此,不要将cancelFunc传递给下游的 goroutines 或函数。Golang 允许你将cancelFunc传递给子 goroutines,但这不是推荐的做法。

  • Golang 中的上下文 * 解释 * Golang 上下文解释 * sidetoc

在 GO 中验证 IP 地址

来源:golangbyexample.com/validate-an-ip-address-in-go/

net 包的 ParseIP 函数可用于验证 IP 地址。该函数可以同时验证 IPV4 和 IPV6 地址。以下是该函数的签名。

func ParseIP(s string) IP

golang.org/pkg/net/#ParseIP

ParseIP 函数

  • 如果给定的 IP 地址无效,则返回 nil。

  • 否则它会创建一个 IP 结构的实例并返回该实例。

让我们看看一个可运行的代码:

package main

import (
    "fmt"
    "net"
)

func main() {
    validIPV4 := "10.40.210.253"

    checkIPAddress(validIPV4)

    invalidIPV4 := "1000.40.210.253"
    checkIPAddress(invalidIPV4)

    valiIPV6 := "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
    checkIPAddress(valiIPV6)

    invalidIPV6 := "2001:0db8:85a3:0000:0000:8a2e:0370:7334:3445"
    checkIPAddress(invalidIPV6)
}

func checkIPAddress(ip string) {
    if net.ParseIP(ip) == nil {
        fmt.Printf("IP Address: %s - Invalid\n", ip)
    } else {
        fmt.Printf("IP Address: %s - Valid\n", ip)
    }
} 

输出:

IP Address: 10.40.210.253 - Valid
IP Address: 1000.40.210.253 - Invalid
IP Address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - Valid
IP Address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:3445 - Invalid

在 Go(Golang)中验证结构体中字段的存在。

来源:golangbyexample.com/struct-field-validate-presence-golang/

目录

  • 概述

  • 第一个库 (go-playground/validator)")

  • 第二个库 (asaskevich/govalidator)")

概述

在本教程中,我们将探讨两个可以用于验证 Golang 中结构体字段的库。这两个库是

对于本教程,我们将使用下面的员工结构体。

type employee struct {
    Name string
}

第一个库 (go-playground/validator)

让我们首先看看 playground 验证库。下面是相应的代码。

go.mod

module sample.com/validate
go 1.14
require (
    github.com/go-playground/universal-translator v0.17.0 // indirect
    github.com/leodido/go-urn v1.2.1 // indirect
    gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
    gopkg.in/go-playground/validator.v9 v9.31.0
)

main.go

package main
import (
    "fmt"
    "gopkg.in/go-playground/validator.v9"
)

var validate *validator.Validate

type employee struct {
    Name string `validate:"required"`
}

func main() {
    e := employee{}
    err := validateStruct(e)
    if err != nil {
        fmt.Printf("Error: %s\n", err)
    }
}

func validateStruct(e employee) error {
    validate = validator.New()
    err := validate.Struct(e)
    if err != nil {
        return err
    }
    return nil
}

输出

Error: Key: 'employee.Name' Error:Field validation for 'Name' failed on the 'required' tag

首先,我们需要声明 Validate 的实例。

var validate *validator.Validate

注意,我们需要将元标签与结构体字段关联,以让验证器知道你想验证这个字段。在上述示例中,我们为 Name 字段添加了标签。该标签由 playground 验证库解释。

type employee struct {
    Name string `validate:"required"`
}

然后调用 Struct 方法来验证结构体。

validate.Struct(e)

当我们将Name字段传递为 nil 时,它会引发正确的错误。

第二个库 (asaskevich/govalidator)

go.mod

module sample.com/validator
go 1.14

require github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef

main.go

package main

import (
	"fmt"

	"github.com/asaskevich/govalidator"
)

type employee struct {
	Name string `valid:"required"`
}

func main() {
	e := employee{}
	err := validateStruct(e)
	if err != nil {
		fmt.Printf("Error: %s\n", err)
	}
}

func validateStruct(e employee) error {
	_, err := govalidator.ValidateStruct(e)
	if err != nil {
		return err
	}
	return nil

}

输出

Error: Name: non zero value required

与上述示例类似,我们将标签与 Name 字段关联,govalidator 可以解释这些标签。

Name string `valid:"required"`

然后我们调用ValidateStruct函数,当我们将Name字段传递为 nil 时,它会引发正确的错误。

posted @ 2024-10-19 08:37  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报