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

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

在 Go(Golang)中发起 HTTP 请求时设置超时

来源:golangbyexample.com/set-timeout-http-golang/

目录

  • 概述

  • 程序

概述

client 结构体来自 HTTP 包,可以用来指定超时。在创建HTTP客户端时,我们可以指定超时的值。关于 HTTP 客户端需要注意的一点是,它只会被创建一次,并且同一个实例会被用于多个 HTTP 请求。以下是http.Client结构体的结构。

type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}

正如你所看到的,可以为http.Client指定超时选项,即Timeout字段。

现在让我们看一个工作示例。

程序

在下面的程序中,我们设置了 1 纳秒的超时,以展示在发起请求时确实会发生超时。

package main
import (
    "fmt"
    "net/http"
    "os"
    "time"
)
func main() {
    client := &http.Client{
        Timeout: time.Nanosecond * 1,
    }
    _, err := client.Get("https://google.com")
    if os.IsTimeout(err) {
        fmt.Println("Timeout Happened")
    } else {
        fmt.Println("Timeout Did not Happened")
    }
}

输出

Timeout Happened

在 Go 语言中设置 http cookie

来源:golangbyexample.com/set-cookie-http-golang/

目录

  • 概述

  • 为某个请求设置 cookie

  • 为该域的所有请求设置 cookie 并发送该 cookie

    • Client

    • Server

概述

在 golang 中,cookie 的表示如下:

golang.org/src/net/http/cookie.go

type Cookie struct {
	Name  string
	Value string

	Path       string    // optional
	Domain     string    // optional
	Expires    time.Time // optional
	RawExpires string    // for reading cookies only

	// MaxAge=0 means no 'Max-Age' attribute specified.
	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
	// MaxAge>0 means Max-Age attribute present and given in seconds
	MaxAge   int
	Secure   bool
	HttpOnly bool
	SameSite SameSite
	Raw      string
	Unparsed []string // Raw text of unparsed attribute-value pairs
}

详见tools.ietf.org/html/rfc6265了解上述 cookie 的每个字段的详细信息。

在设置 cookies 时,有两种情况。

  • 你希望只为特定域的一个请求设置 cookie。

  • 你希望设置 cookies,并希望该 cookie 在所有请求中发送到该域。

第二个用例是在第一次调用中通过输入用户名和密码生成身份验证令牌时需要的,你希望该令牌在每个后续调用中通过 cookie 传递。我们将为此用例使用 cookie jar。

让我们逐一查看这两种用例。

这是 golang 作为 HTTP 客户端的情况。可以使用net/http包的AddCookie方法添加 cookie。如果我们为两个不同的名称和值调用此方法,则这两个名称和值将被添加到结果 cookie 中。

package main
import (
    "fmt"
    "log"
    "net/http"
    "net/http/cookiejar"
)
var client http.Client
func init() {
    jar, err := cookiejar.New(nil)
    if err != nil {
        log.Fatalf("Got error while creating cookie jar %s", err.Error())
    }
    client = http.Client{
        Jar: jar,
    }
}
func main() {
    cookie := &http.Cookie{
        Name:   "token",
        Value:  "some_token",
        MaxAge: 300,
    }
    cookie2 := &http.Cookie{
        Name:   "clicked",
        Value:  "true",
        MaxAge: 300,
    }
    req, err := http.NewRequest("GET", "http://google.com", nil)
    if err != nil {
        log.Fatalf("Got error %s", err.Error())
    }
    req.AddCookie(cookie)
    req.AddCookie(cookie2)
    for _, c := range req.Cookies() {
        fmt.Println(c)
    }
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("Error occured. Error is: %s", err.Error())
    }
    defer resp.Body.Close()
    fmt.Printf("StatusCode: %d\n", resp.StatusCode)
}

输出

token=some_token
clicked=true
StatusCode: 200

我们首先需要创建 cookie 的实例,如下所示。

golang.org/src/net/http/cookie.go

cookie := &http.Cookie{
    Name:   "token",
    Value:  "some_token",
    MaxAge: 300,
}

一旦创建了 cookie,就使用AddCookie方法将其添加到 HTTP 请求对象中。

req.AddCookie(cookie)

在上述程序中,HTTP 客户端添加了两个带有以下名称-值对的 cookie。

  • token=some_token

  • clicked=true

这两个 cookie 将在对google.com的调用中发送。

golang 中的 HTTP 客户端允许你指定一个CookieJar,可用于在进行外部 HTTP 请求时存储和发送 cookies。顾名思义,可以把它想象成一个装有 cookies 的罐子。

golang.org/pkg/net/http/#Client

以下是 net/http Client结构的结构。它包含一个名为Jar的实例变量,类型为CookieJar,这是一个接口。

type Client struct {
    Transport RoundTripper

    CheckRedirect func(req *Request, via []*Request) error

    Jar CookieJar

    Timeout time.Duration
}

以下是CookieJar接口

type CookieJar interface {
    SetCookies(u *url.URL, cookies []*Cookie)
    Cookies(u *url.URL) []*Cookie
}

net/http 提供了一个默认的 cookie jar 实现,符合上述CookieJar接口。我们将在初始化我们的 net/http 客户端时使用它。

golang.org/pkg/net/http/cookiejar/#Jar

你还可以在初始化 net/http Client 结构时提供自定义 cookie jar,该结构实现了上述CookieJar接口。

HTTP 客户端以两种方式使用这个 jar。

  • 正在向这个 Jar 添加 cookies。你可以明确地将 cookies 添加到这个 jar 中。如果服务器在响应头中发送 Set-Cookies 头,cookies 也会被添加到 jar 中。在Set-Cookie头中指定的所有 cookies 都会被添加。有关 Set-Cookie 头的更多细节,请参阅这个链接。这个链接包含了理解 golang 中Set-Cookie头的所有细节。

  • 在进行任何外部 HTTP 请求时,咨询这个 jar。它会检查这个 jar,以了解需要为特定域发送哪些 cookies。

让我们看看一个示例,客户端在进行 HTTP 请求时将添加一个 cookie。这个 cookie 将会在所有后续请求中发送到同一域。

在这个示例中,我们将看到客户端如何在 cookie jar 中设置 cookie。为此,让我们首先创建一个客户端。以下是相应的程序。

这是客户端代码。

客户端

go.mod

module sample.com/client
go 1.16

client.go

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
)

var client http.Client

func init() {
	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Fatalf("Got error while creating cookie jar %s", err.Error())
	}

	client = http.Client{
		Jar: jar,
	}
}

func main() {
	req, err := http.NewRequest("GET", "http://localhost:8080/doc", nil)
	if err != nil {
		log.Fatalf("Got error %s", err.Error())
	}
	cookie := &http.Cookie{
		Name:   "token",
		Value:  "some_token",
		MaxAge: 300,
	}
	urlObj, _ := url.Parse("http://localhost:8080/")
	client.Jar.SetCookies(urlObj, []*http.Cookie{cookie})
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Error occured. Error is: %s", err.Error())
	}
	defer resp.Body.Close()

	fmt.Printf("StatusCode: %d\n", resp.StatusCode)

	req, err = http.NewRequest("GET", "http://localhost:8080/doc/id", nil)
	if err != nil {
		log.Fatalf("Got error %s", err.Error())
	}

	resp, err = client.Do(req)
	if err != nil {
		log.Fatalf("Error occured. Error is: %s", err.Error())
	}
	defer resp.Body.Close()

	fmt.Printf("StatusCode: %d\n", resp.StatusCode)
}

在上述客户端程序中,我们正在创建一个带有 cookie Jar 的 HTTP 客户端。

jar, err := cookiejar.New(nil)
if err != nil {
     log.Fatalf("Got error while creating cookie jar %s", err.Error())
}

client = http.Client{
     Jar: jar,
}

我们正在向 Cookie Jar 添加一个 cookie。

cookie := &http.Cookie{
		Name:   "token",
		Value:  "some_token",
		MaxAge: 300,
}
urlObj, _ := url.Parse("http://localhost:8080/")
client.Jar.SetCookies(urlObj, []*http.Cookie{cookie})

为了测试上述代码,并说明在第一次调用中添加到 cookie jar 中的 cookies 确实会在后续调用中发送,我们还需要创建服务器,以便打印传入的 cookies。

服务器

服务器监听 8080 端口并有两个 API。这是客户端调用的两个 API。

  • localhost:8080/doc

  • localhost:8080/doc/id

在这两个 API 中,我们打印接收到的传入头中的 cookies。

go.mod

module sample.com/server

go 1.16

server.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) {
	fmt.Println("Cookie in First API Call")
	for _, c := range r.Cookies() {
		fmt.Println(c)
	}
	fmt.Println()
	w.WriteHeader(200)
	w.Write([]byte("Doc Get Successful"))
	return
}

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

现在运行服务器。

go run server.go

以及客户端。

go run client.go

注意服务器端的输出。

Cookie in First API Call
token=some_token

Cookie in Second API Call
token=some_token

同样的 cookie 会在客户端对服务器的第一次和第二次调用中自动发送。这是如何开箱即用的?这是因为CookieJar的介入。golang HTTP 客户端在进行 HTTP 调用之前会检查 Cookie Jar,然后发送这个 cookie。

这就是在 golang 中设置 cookies 的全部内容。希望你喜欢这个教程。请在评论中分享反馈。

此外,请查看我们的 Golang 高级教程系列 – Golang 高级教程

Golang 中的集合实现

来源:golangbyexample.com/set-implementation-in-golang/

集合是一种数据结构,它无特定顺序地存放元素。元素在集合中只出现一次。

集合可以在 GO 中使用地图实现。我们将使用map[string]struct{}作为集合,因为struct{}占用的内存为零,因此在存储方面更高效。

以下是具有操作的集合的简单示例

  1. 添加

  2. 删除

  3. 存在

package main

import (
    "fmt"
)

//MakeSet initialize the set
func makeSet() *customSet {
    return &customSet{
        container: make(map[string]struct{}),
    }
}

type customSet struct {
    container map[string]struct{}
}

func (c *customSet) Exists(key string) bool {
    _, exists := c.container[key]
    return exists
}

func (c *customSet) Add(key string) {
    c.container[key] = struct{}{}
}

func (c *customSet) Remove(key string) error {
    _, exists := c.container[key]
    if !exists {
        return fmt.Errorf("Remove Error: Item doesn't exist in set")
    }
    delete(c.container, key)
    return nil
}

func (c *customSet) Size() int {
    return len(c.container)
}

func main() {
    customSet := makeSet()
    fmt.Printf("Add: B\n")
    customSet.Add("A")
    fmt.Printf("Add: B\n")
    customSet.Add("B")
    fmt.Printf("Size: %d\n", customSet.Size())
    fmt.Printf("A Exists?: %t\n", customSet.Exists("A"))
    fmt.Printf("B Exists?: %t\n", customSet.Exists("B"))
    fmt.Printf("C Exists?: %t\n", customSet.Exists("C"))
    fmt.Printf("Remove: B\n")
    customSet.Remove("B")
    fmt.Printf("B Exists?: %t\n", customSet.Exists("B"))
}

输出:

Add: B
Add: B
Size: 2
A Exists?: true
B Exists?: true
C Exists?: false
Remove: B
B Exists?: false

在 Go (Golang)中,如果行或列为零,则设置矩阵为零

来源:golangbyexample.com/set-matrix-zero-golang/

目录

  • 概述

  • 程序 * * ## 概述

给定一个 m*n 的矩阵。如果一个元素为零,则将其行和列设置为零

示例

输入:

1, 1, 1 
0, 1, 1 
1, 1, 1

输出:

0, 1, 1 
0, 0, 0 
0, 1, 1

我们将通过使用长度分别为mn的两个额外数组rowSetcolumnSet来解决这个问题。我们将遍历矩阵并

if matrix[i][j] == 0 then
   rowSet[i] = 1
   columnSet[j] = 1

如果rowSet[i]等于 1 或columnSet[j]等于 1,则可以将matrix[i][j]设为零。

程序

这是相应的程序。

package main

import "fmt"

func setZeroes(matrix [][]int) [][]int {
	numRows := len(matrix)

	numColumns := len(matrix[0])

	rowSet := make([]int, numRows)
	columnSet := make([]int, numColumns)

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if matrix[i][j] == 0 {
				rowSet[i] = 1
				columnSet[j] = 1
			}
		}
	}

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if rowSet[i] == 1 || columnSet[j] == 1 {

				matrix[i][j] = 0

			}
		}
	}

	return matrix

}

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

输出

[[0 1 1] [0 0 0] [0 1 1]]

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

如果你对了解如何在 Golang 中实现所有设计模式感兴趣。那么这篇文章就是为你准备的 - 所有设计模式 Golang

在 Golang 中设置、取消设置或获取环境变量

来源:golangbyexample.com/set-unset-get-env-variable-golang/

目录

  • 概述

    • 设置环境值

    • 获取环境值

    • 取消设置环境值

    • 查找环境变量

  • 代码:

概述

os包提供了一些与操作 Go 中的环境变量相关的实用方法。

设置环境值

func Setenv(key, value string) error

获取环境值

如果环境变量的Getenv()函数返回的值为空,表示该环境变量不存在。要区分未设置和为空的环境变量,请使用 LookupEnv()函数,它会返回额外的布尔值,指示环境变量是否存在。

func Getenv(key string) string

取消设置环境值

func Unsetenv(key string) error 

查找环境变量

从下面的签名可以明显看出,LookupEnv 返回额外的布尔值。

  • 如果key代表的环境变量存在,则返回其值,返回的布尔值为真。

  • 如果 key 代表的环境变量不存在,则返回的值为空,返回的布尔值为假。

func LookupEnv(key string) (string, bool)

代码:

package main

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

func main() {
    //Set env a to b
    err := os.Setenv("a", "b")
    if err != nil {
        log.Fatal(err)
    }

    //Get env a
    val := os.Getenv("a")
    fmt.Println(val)

    //Unset a
    err = os.Unsetenv("a")
    if err != nil {
        log.Fatal(err)
    }

    val, present := os.LookupEnv("a")
    fmt.Printf("a env variable present: %t\n", present)
}

输出:

b
a env variable present: false
```*


<!--yml

category: 未分类

date: 2024-10-13 06:32:02

-->

# 在 Go (Golang) 中为输出的 HTTP 请求设置请求头部

> 来源:[`golangbyexample.com/set-headers-http-request/`](https://golangbyexample.com/set-headers-http-request/)

注意:相关帖子

+   从传入的 HTTP 请求中获取头部信息 – [`golangbyexample.com/headers-http-request-golang/`](https://golangbyexample.com/headers-http-request-golang/)

+   在 Go (Golang) 中为传入的 HTTP 请求设置响应头部 – [`golangbyexample.com/set-resposne-headers-http-go/`](https://golangbyexample.com/set-resposne-headers-http-go/)

+   获取 Golang 中的输出 HTTP 请求的响应头部 - [`golangbyexample.com/get-response-headers-making-go/`](https://golangbyexample.com/get-response-headers-making-go/)

现在让我们看看如何在进行 HTTP 请求时设置头部

目录

+   概述

+   使用 r.Header.Add() 方法 方法")

+   使用 r.Header.Set 方法

+   示例

# **概述**

以下是头部在 Go 中表示的格式。

```go
type Header map[string][]string

所以头部是一个键值对

  • 键以规范形式表示。规范形式意味着第一个字符和任何在连字符后的字符都是大写。所有其他字符为小写。规范形式的示例为
Content-Type
Accept-Encoding
  • 值表示为字符串切片。为什么使用字符串数组?因为在请求中用相同键和不同值的两个头部是完全可以的。两个值将被收集到切片中。

例如,如果在客户端的输出请求中设置了以下头部,则

user-agent: goapplication
foo: bar1
foo: bar2

然后在服务器上,头部将如下所示

User-Agent: goapplication
Foo: bar1
Foo: bar2

请注意:

  • user-agent 被转换为规范形式 User-Agent

  • foo 头部被转换为 Foo。还要注意,将在请求中设置两个具有相同键 foo 的头部。其值将是 bar1bar2

现在我们已经看到头部在请求中的表示。让我们看看在进行 HTTP 请求时如何设置请求头部值。假设我们有以下需要在 HTTP 请求中设置的键值对头部

user-agent: goapplication
foo: bar1
foo: bar2

Headerhttp.Request 结构中如下所示。

type Request struct {
    Method string
    Header Header
    ...
}

在下面的示例中,假设变量 r 的类型为 *http.Request。现在让我们看看设置头部的不同方式

使用 r.Header.Add() 方法

以下是 Add 方法的签名

func (h Header) Add(key, value string)

此方法用于将键值对添加到请求头部。我们已经看到,头部值也可以是数组。因此,此方法将附加到可能已与该键关联的现有值上。此外,键将被转换为规范形式。例如,如果我们用不同的值两次添加 foo 头部

r.Header.Add("foo", "bar1")
r.Header.Add("foo", "bar2")

然后foo头将被设置为[“bar1”, “bar2”]。同时,foo在进行 HTTP 调用之前将变为Foo

使用 r.Header.Set 方法

以下是函数的签名

func (h Header) Set(key, value string)

它可用于设置与给定键相关联的头部条目。与Add方法不同,此方法会替换与该键相关联的任何现有值。同时,键将被转换为规范形式。

示例

让我们看看一个程序,阐明以上所有要点

package main
import (
    "fmt"
    "net/http"
    "time"
)
func main() {
    call("https://google.com", "GET")
}
func call(url, method string) error {
    client := &http.Client{
        Timeout: time.Second * 10,
    }
    req, err := http.NewRequest(method, url, nil)
    if err != nil {
        return fmt.Errorf("Got error %s", err.Error())
    }
    req.Header.Set("user-agent", "golang application")
    req.Header.Add("foo", "bar1")
    req.Header.Add("foo", "bar2")
    response, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("Got error %s", err.Error())
    }
    defer response.Body.Close()
    return nil
}

请注意在上述示例中我们在进行 HTTP 调用时如何设置头部

req.Header.Set("user-agent", "goapplication")
req.Header.Add("foo", "bar1")
req.Header.Add("foo", "bar2")

在 Go (Golang) 中为传入的 HTTP 请求设置响应头。

来源:golangbyexample.com/set-resposne-headers-http-go/

注意:相关帖子。

现在让我们看看如何在传入的 HTTP 请求中设置响应头。

目录。

概述

  • 使用 w.Header().Add() 方法.Add() 方法")。

  • 使用 w.Header().Set 方法.Set 方法")。

  • 示例

概述

下面是头部在 Go 中表示的格式。

type Header map[string][]string

因此,头部是一个键值对,包含:

  • 键以规范形式表示。规范形式意味着第一个字符和任何在连字符后面的字符都为大写。其他所有字符均为小写。规范形式的示例有:
Content-Type
Accept-Encoding
  • 值表示为字符串切片。为什么是字符串数组?因为在请求中同一个键可以有两个不同值的头部是完全可以接受的。两个值将被收集到切片中。

例如,如果以下头部在服务器的外发响应中设置,则:

content-type: applcation/json
foo: bar1
foo: bar2

然后在客户端,头部将如下所示:

Content-Type: applcation/json
Foo: bar1
Foo: bar2

注意事项:

  • content-type 被转换为规范形式 Content-Type

  • foo 头部被转换为 Foo。还要注意,响应中会有两个相同键的头部,即 foo。它们的值将是 bar1bar2

现在我们已经看到头部在请求中的表示。让我们看看如何设置响应头的值。假设我们有以下需要在响应头中设置的键值对:

content-type: applcation/json
foo: bar1
foo: bar2

http.ResponseWriter 接口中定义了一个 Header 方法,如下所示。它返回与响应相关联的头部。

type ResponseWriter interface {
    Header() Header
    ...
}

在下面的示例中,假设变量 w 的类型为 http.ResponseWriter。现在让我们看看设置头部的不同方法。

使用 w.Header().Add() 方法

下面是 Add 方法的签名。

func (h Header) Add(key, value string)

此方法用于将键值对添加到响应头中。我们已经看到上面的头部值也可以是数组。因此,此方法将附加到可能已经与该键关联的现有值上。该键也将被转换为规范形式。

例如,如果我们用不同的值添加 foo 头部两次:

w.Header().Add("foo", "bar1")
w.Header().Add("foo", "bar2")

然后foo头部将被设置为[“bar1”, “bar2”]。同时,在向客户端发送响应时,foo将变为Foo

使用 w.Header().Set 方法

以下是该函数的签名。

func (h Header) Set(key, value string)

它可用于设置与给定键相关的头部条目。与Add方法不同,此方法将替换与该键相关的任何现有值。同时,该键将转换为规范形式。

示例

让我们看看一个展示以上所有要点的程序。

package main

import (
	"encoding/json"
	"net/http"
)

func main() {
	handler := http.HandlerFunc(handleRequest)
	http.Handle("/example", handler)
	http.ListenAndServe(":8085", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("content-type", "application/json")

	w.Header().Add("foo", "bar1")
	w.Header().Add("foo", "bar2")

	resp := make(map[string]string)
	resp["message"] = "success"
	jsonResp, _ := json.Marshal(resp)
	w.Write(jsonResp)
}

在上述程序中,我们启动了一个监听在 8085 端口的服务器。我们将content-type头部设置如下。

w.Header().Set("content-type", "application/json")

我们将foo头部设置如下。

w.Header().Add("foo", "bar1")
w.Header().Add("foo", "bar2")

我们还在该端点定义了一个 URL。运行此服务器并进行以下 API 调用。

curl -v -X POST http://localhost:8085/example

运行此 API 后,请在终端检查响应头。它与我们讨论的内容完全一致。此外,每个头部键都转换为其规范形式。

< HTTP/1.1 200 OK
< Content-Type: application/json
< Foo: bar1
< Foo: bar2
< Content-Length: 21
< 
* Connection #0 to host localhost left intact
{"message":"success"}

设置 GO 工作区和 Hello World 程序

来源:golangbyexample.com/workspace-hello-world-golang/

这是 golang 综合教程系列的第三章。请参考此链接获取系列的其他章节 – Golang 综合教程系列

下一个教程包和模块 – 第一部分

上一个教程GO 安装

现在让我们查看当前的教程。以下是当前教程的目录。

目录

  • 概述

  • GO 工作区

    • GOROOT

    • GOPATH

    • GOBIN

  • 运行 Hello World

  • 结论:

概述

这是 golang 综合教程系列的第三个教程。请参考此链接获取系列的其他章节。

在第一章中,我们看到了 Go 的介绍和 GO 的基本优点,而在第二章中,我们已经看到如何安装 GO。现在是时候为 GO 设置工作区并运行我们的第一个 Hello World 程序了。

GO 工作区

在继续之前,让我们理解一些重要的 GO 环境变量:

GOROOT

它是你的 GO SDK 的位置。GO 二进制发行版假设这个位置为 Linux/MAC 平台的/usr/local/go和 Windows 的 c:\Go。但 GO SDK 也可以安装在自定义位置。如果安装在自定义位置,则 GOROOT 应指向该目录。GOROOT 仅在将 GO 安装到自定义位置或希望维护不同版本的 GO 时设置。

例如,如果你在 Linux/MAC 平台上将 GO 安装到位置/Documents/go,那么需要在/$HOME/.profile 文件中添加以下条目。

export GOROOT=~/Documents/go
export PATH=$PATH:$GOROOT/bin

GOPATH

对于GOPATH的讨论将围绕你使用的 GO 版本是 1.13 及以上还是低于 1.13。在 1.13 版本中,GO 引入了一种新的依赖管理特性,称为 GO 模块。我们首先了解GOPATH的传统行为,然后再讨论在 GO 1.13 版本后与GOPATH有关的变化。

在 GO 1.13 版本之前

GOPATH环境变量用于解析 go 导入语句,同时还指定你的 GO 工作区的位置。它是 GO 工作区的根目录。如果依赖项不在 GOPATH 内,则无法导入。因此,早期版本要求你的所有源代码都在GOPATH内。所以基本上GOPATH是用来

  • 解析导入。如果依赖项不在 GOPATH 内,则无法导入。因此,它仍然要求你的代码在 GOPATH 内。

  • 包安装在目录\(GOPATH/pkg/\)GOOS_$GOARCH 中。

  • 将编译的应用程序二进制文件存储在\(GOPATH/bin 中(可以通过将\)GOBIN 设置为不同路径来覆盖)。

它包含以下文件夹。

  • src

    • 源文件位置。它包含.go 和其他源文件。

    • 使用‘go get’安装任何依赖包时,所有包文件都存储在此位置。

  • pkg

    • 存储你在 src 目录中实际源代码的编译输出。它包含.a 文件。

    • 基本上,它包含从 src/目录编译的 GO 包。然后在链接时使用它们来创建二进制可执行文件,这些文件将放置在 bin/目录中。

    • 编译一个包一次并用于创建不同的二进制可执行文件是个好主意。

    • 每对操作系统和架构将在 pkg 中有自己的子目录(例如:pkg/GOOS_GOARCH)。

  • bin – Go 构建的可执行文件位置。

使用 Go 版本 1.13 或更高版本

在版本1.13中,GO 宣布了一种新的依赖管理特性,称为 GO 模块。我们将在接下来的教程中学习这一特性。现在,你可以想象,有了新特性,GO 不再要求将所有 GO 代码放在 Go 工作空间或$GOAPTH/src 目录中。你可以在任何地方创建目录并将 Go 程序放在那里。请注意,GOPATH 的所有传统行为在 1.1.3 及更高版本中仍然有效。同时请注意,使用这个新的 GO 模块特性,GO 程序可以以两种方式运行。

  • 使用模块:在使用模块时,GOPATH不用于解析导入。然而,在使用模块运行 GO 程序时,GOPATH 将被使用。

    • 将下载的源代码存储在$GOPATH/pkg/mod 目录中。

    • 将编译的应用程序二进制文件存储在\(GOPATH/bin 中(可以通过将\)GOBIN 设置为不同路径来覆盖)。

  • 不使用模块:即使是 1.13 版本或更高版本,仍然可以以传统方式运行 GO 程序。在不使用模块的情况下运行 GO 程序时,$GOPATH 的行为与早期版本相同,如上所述。

设置 GOPATH。

如果此环境变量未设置,则在 Unix 系统上默认为\(HOME/go**,在 Windows 上默认为**%USERPROFILE%\go**。如果你的工作空间位置是~**/Desktop/go**,那么在**~/\)HOME/.profile文件中添加以下条目是个好主意。始终设置GOPATH是个好主意,因为即使引入模块,GOPATH也会被使用。

export GOPATH=~/Desktop/go

GOBIN

GOBIN环境变量指定了 go 在构建主包后放置编译应用程序二进制文件的目录。如果GOPATH环境变量未设置,则默认为\(GOPATH/bin**或**\)HOME/go/bin。此外,将GOBIN路径添加到PATH环境中也是个好主意,这样安装的二进制文件可以在不指定完整路径的情况下运行。在~/.$HOME/.profile文件中设置 GOBIN,并将其添加到 PATH 中。

export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN

运行 Hello World

在这个例子中,我们将创建我们的第一个 hello world 程序。目前GOROOTGOPATHGOBIN的值如下。

echo $GOROOT will give installed directory for GO
echo $GOPATH will give ~/Desktop/go
echo $GOBIN will give ~/Desktop/go/bin

一个典型的程序具有.go文件扩展名。现在让我们创建一个“Hello World”程序。为此,首先在\(GOPATH/src 外部创建一个名为 hello 的目录。由于项目是在\)GOPATH/src 外部创建的,我们还需要创建一个导入路径为sample.com/hellogo.mod文件。我们将在即将到来的教程中详细了解导入路径、go.mod 文件和模块。现在,让我们创建一个简单的模块,以便查看 hello world 程序在 go 中的样子。请使用以下命令:

go mod init sample.com/hello

它将创建如下的 go.mod 文件。

go.mod

module sample.com/hello

go 1.14

现在创建 hello.go 文件,内容如下。

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

运行上述 GO 程序有三种不同的命令。首先切换到‘hello’目录。

  1. go install’

它将编译程序并将二进制文件放置在$GOBIN文件夹中。输入以下命令,确保你在包含hello.go文件的hello目录中。

它将在$GOBIN 目录中创建一个名为 hello 的二进制文件。二进制文件的名称与模块的导入路径最后一部分相同。模块的导入路径是sample.com/hello,导入路径的最后一部分简单地是hello。因此,在我们的例子中,二进制文件名称将是hello。在终端中输入 hello,将会输出如下内容:

Hello World

‘which hello’的输出将是/Desktop/go/bin/hello’**。记住我们的**GOBIN**路径是‘**/Desktop/go/bin’。因此,二进制文件 hello 被复制到此目录。

  1. go build’

它将编译程序并将二进制文件放置在当前工作目录中。输入以下命令:

go build

在我们的例子中,它将创建一个名为‘hello’的二进制文件,位于hello.go文件旁边。要运行该二进制文件,请在终端中输入‘./hello’。将会输出如下内容:

Hello World
  1. ‘go run hello.go’

该命令将编译并执行二进制文件。输入以下命令。

go run hello.go 

它将输出。

Hello World

结论:

本教程到此结束。希望你喜欢这篇文章。请在评论中分享反馈/错误/改进建议。

下一个教程 包和模块 – 第一部分 上一个教程 GO 安装*

在 Go (Golang) 中洗牌一个切片或数组

来源:golangbyexample.com/shuffle-slice-or-array-go/

目录

  • 概述

  • 代码:

概述

Go 的 math/rand 包提供了一个 Shuffle 方法,可以用于洗牌数组或切片。该方法使用默认源伪随机化元素的顺序。伪随机化意味着对于固定的输入种子,它将生成相同的随机化。这就是为什么在我们的程序中,每次我们会使用不同的种子初始化 rand 包。

以下是函数的签名。

func Shuffle(n int, swap func(i, j int))

该函数接受参数

  • 第一个是数组或切片的长度。

  • 第二个是一个交换函数,它会被调用用于不同的索引 ij。你需要提供自己的交换函数,用于交换数组中的元素。

另外请注意,如果 n<0,这个函数会引发恐慌。让我们来看一下代码。

代码:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().Unix())

    in := []int{2, 3, 5, 8}
    rand.Shuffle(len(in), func(i, j int) {
        in[i], in[j] = in[j], in[i]
    })
    fmt.Println(in)

    rand.Shuffle(len(in), func(i, j int) {
        in[i], in[j] = in[j], in[i]
    })
    fmt.Println(in)
}

输出:

它可能会在你的机器上产生不同的输出。

[5 3 2 8]
[3 5 8 2]

在 Go 中打乱字符串 (Golang)

来源:golangbyexample.com/shuffle-string-golang/

目录

  • 概述

  • 代码:

概述

math/rand 包提供了一个 Shuffle 方法,可以用来打乱字符串。该方法使用默认源对元素的顺序进行伪随机化。伪随机化意味着对于固定的输入种子,它将生成相同的随机化。因此,在我们的程序中,我们会每次用不同的种子初始化 rand 包。

以下是函数的签名。

func Shuffle(n int, swap func(i, j int))

此函数接受以下参数

  • 首先是给定字符串中的总字符数。

  • 第二个是一个交换函数,将为不同的索引 ij 被调用。你需要提供自己的交换函数来交换字符串中的元素。

还要注意,如果 n<0,此函数将引发恐慌。在 Golang 中,字符串是字节序列。字符串字面量实际上表示一个 UTF-8 字节序列。在 UTF-8 中,ASCII 字符是单字节,对应前 128 个 Unicode 字符。所有其他字符的字节数在 1 到 4 之间。因此,无法在字符串中对字符进行索引。在 GO 中,rune 数据类型表示一个 Unicode 点。一旦字符串转换为符文数组,就可以在该数组中对字符进行索引。

你可以在这里了解更多关于上述问题的信息 – golangbyexample.com/number-characters-string-golang/

因此,在下面的程序中,我们首先将字符串转换为符文数组,以便我们可以通过索引访问符文数组,并使用该索引交换字符串中的字符以打乱字符串。

代码:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().Unix())

    in := "abcdedf£"

    inRune := []rune(in)
    rand.Shuffle(len(inRune), func(i, j int) {
        inRune[i], inRune[j] = inRune[j], inRune[i]
    })
    fmt.Println(string(inRune))

    rand.Shuffle(len(inRune), func(i, j int) {
        inRune[i], inRune[j] = inRune[j], inRune[i]
    })
    fmt.Println(string(inRune))
}

输出:

它将在你的机器上产生不同的输出。

dd£cebaf
feb£cadd

Go(Golang)中的单例设计模式

来源:golangbyexample.com/singleton-design-pattern-go/

注意:有兴趣了解如何在 GO 中实现所有其他设计模式。请查看这个完整参考 - Go 中的所有设计模式(Golang)

简介:

单例设计模式是一种创建型设计模式,也是最常用的设计模式之一。当只应存在一个结构体实例时,使用此模式。这个单一实例称为单例对象。单例对象适用的一些情况:

  1. DB 实例 - 我们只想创建一个 DB 对象的实例,并且该实例将在整个应用程序中使用。

  2. 日志实例 - 仍然应该只创建一个日志实例,并在整个应用程序中使用。

当结构体首次初始化时,创建单例实例。通常,在结构体上定义一个 getInstance()方法,只需要创建一个实例。一旦创建,每次调用getInstance()时返回的都是同一单例实例。

在 GO 中我们有 goroutines。因此,单例结构体应该在多个 goroutines 尝试访问该实例时返回相同的实例。实现单例设计模式很容易出错。下面的代码说明了创建单例对象的正确方法。

var lock = &sync.Mutex{}

type single struct {
}

var singleInstance *single

func getInstance() *single {
    if singleInstance == nil {
        lock.Lock()
        defer lock.Unlock()
        if singleInstance == nil {
            fmt.Println("Creting Single Instance Now")
            singleInstance = &single{}
        } else {
            fmt.Println("Single Instance already created-1")
        }
    } else {
        fmt.Println("Single Instance already created-2")
    }
    return singleInstance
}

上述代码确保只创建一个单一结构体的实例。有几点值得注意。

  1. 开始时有一个对 nil singleInstance的检查。这是为了防止每次调用getInstance()方法时进行昂贵的锁操作。如果这个检查失败,则意味着singleInstance已经创建。

  2. singleInstance是在锁内创建的。

  3. 锁定后还有一个对 nil singleInstance的检查。这是为了确保如果多个 goroutine 绕过第一次检查,那么只有一个 goroutine 能够创建单例实例,否则每个 goroutine 将创建自己的single结构体实例。

这是完整代码

single.go

package main

import (
    "fmt"
    "sync"
)

var lock = &sync.Mutex{}

type single struct {
}

var singleInstance *single

func getInstance() *single {
    if singleInstance == nil {
        lock.Lock()
        defer lock.Unlock()
        if singleInstance == nil {
            fmt.Println("Creting Single Instance Now")
            singleInstance = &single{}
        } else {
            fmt.Println("Single Instance already created-1")
        }
    } else {
        fmt.Println("Single Instance already created-2")
    }
    return singleInstance
}

main.go

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 100; i++ {
        go getInstance()
    }
    // Scanln is similar to Scan, but stops scanning at a newline and
    // after the final item there must be a newline or EOF.
    fmt.Scanln()
}

输出:

Creting Single Instance Now
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-2
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1

注意:

  • 有一个输出为"现在创建单实例",这意味着只有一个 goroutine 能够创建单一实例。

  • 有几个输出为"单实例已创建-1",这意味着一些 goroutines 在第一次检查中发现 singleInstance 的值为 nil 并绕过了它。

  • 有几个输出为"单实例已创建-2",这意味着当它们到达时,单实例已经创建,它们无法绕过第一次 if 检查。

在 Go 中创建单例对象的其他方法

  • init()函数

我们可以在 init 函数内创建一个单实例。这仅适用于对象的提前初始化是可以的。init 函数在一个包的每个文件中只会被调用一次,因此我们可以确保只创建一个单实例。

  • sync.Once

sync.Once 只会执行一次操作。请参见下面的代码。

single.go

var once sync.Once

type single struct {
}

var singleInstance *single

func getInstance() *single {
    if singleInstance == nil {
        once.Do(
            func() {
                fmt.Println("Creting Single Instance Now")
                singleInstance = &single{}
            })
    } else {
        fmt.Println("Single Instance already created-2")
    }
    return singleInstance
}

输出:

Creting Single Instance Now
Single Instance already created-2
Single Instance already created-2
  • 有一个输出为“现在创建单实例”,这意味着只有一个 goroutine 能够创建单实例。

  • 有几个输出为“单实例已创建-2”,这意味着在到达时单实例已经被创建,他们无法绕过第一次 if 检查。

  • 单例 * Golang 中的单例设计模式

Go 语言中的切片 (Golang)

来源:golangbyexample.com/slice-in-golang/

这是 golang 综合教程系列的第十八章。有关该系列其他章节,请参阅此链接 – Golang 综合教程系列

下一个教程 – 映射

上一个教程 – 数组

现在让我们查看当前的教程。以下是当前教程的目录。

目录

  • 概述

  • 创建切片

    • 使用 []{} 格式

    • 从另一个切片或数组创建切片

      • 从数组创建切片

      • 从切片创建切片

    • 使用 make 函数

    • 使用 new 函数

  • 长度与容量

  • 访问和修改切片元素

  • 切片的不同迭代方式

  • 向切片添加元素

  • 复制切片

  • 空切片

  • 多维切片

  • 总结

概述

数组的大小限制了其表达能力和在 Go 中的力量。这就是切片的用武之地。切片比数组更强大和方便使用。实际上,切片与其他编程语言中的数组更为相似。

切片指向底层数组,并通过切片头进行内部表示。与数组不同,切片的大小是灵活的,可以更改。

切片的内部表示

切片在内部由三部分表示。

  • 指向底层数组的指针

  • 底层数组的当前长度

  • 总容量是底层数组可以扩展到的最大容量。

上述内部表示由SliceHeader结构描述,结构如下所示:

type SliceHeader struct {
        Pointer uintptr
        Len  int
        Cap  int
}

切片头中的指针字段是指向底层数组的指针。Len 是切片的当前长度,Cap 是切片的容量。与数组类似,切片索引从开始,到length_of_slice-1结束。因此,一个长度为 3、容量为 5 的切片如下所示:

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

创建 切片

创建切片有四种方式。

  • 使用 []<类型>{} 格式

  • 从另一个切片或数组创建切片。

  • 使用 make

  • 使用 new

让我们逐个看一下上述每种方法。

使用 []<类型>{} 格式

声明切片的最常用方法是这个。

s := []int

它声明了一个长度为 0、容量为 0 的空切片。我们也可以在声明时初始化切片。

s := []int{1,2}

它声明了一个长度为 2 的整数切片,容量也为 2。容量将等于指定的实际切片元素数量。我们还有两个由 Go 提供的库函数可以用来获取切片的 长度容量

  • len() 函数 – 用于切片的长度。

  • cap() 函数 – 用于切片的容量。

让我们看看一个展示上述要点的小程序。

package main

import "fmt"

func main() {
    sample := []int{}
    fmt.Println(len(sample))
    fmt.Println(cap(sample))
    fmt.Println(sample)

    letters := []string{"a", "b", "c"}
    fmt.Println(len(letters))
    fmt.Println(cap(letters))
    fmt.Println(letters)
}

输出

0
0
[]

3
3
[a b c]

当实际元素未指定时,切片的长度和容量均为零。当实际元素被指定时,长度和容量等于指定的实际元素数量。

从另一个切片或数组创建切片

切片可以通过重新切片现有切片或数组来创建。

从数组创建切片

通过重新切片现有数组创建新切片的格式是

[n]sample[start:end]

上述操作将返回一个从索引 开始 到索引 结束-1 的新切片。因此,索引 结束 处的元素不包括在新创建的切片中。在重新切片时,起始和结束索引都是可选的。

  • 起始索引的默认值为零。

  • 结束索引的默认值是数组的长度。

让我们看一个例子。

package main

import "fmt"

func main() {
    numbers := [5]int{1, 2, 3, 4, 5}

    //Both start and end
    num1 := numbers[2:4]
    fmt.Println("Both start and end")
    fmt.Printf("num1=%v\n", num1)
    fmt.Printf("length=%d\n", len(num1))
    fmt.Printf("capacity=%d\n", cap(num1))

    //Only start
    num2 := numbers[2:]
    fmt.Println("\nOnly start")
    fmt.Printf("num1=%v\n", num2)
    fmt.Printf("length=%d\n", len(num2))
    fmt.Printf("capacity=%d\n", cap(num2))

    //Only end
    num3 := numbers[:3]
    fmt.Println("\nOnly end")
    fmt.Printf("num1=%v\n", num3)
    fmt.Printf("length=%d\n", len(num3))
    fmt.Printf("capacity=%d\n", cap(num3))

    //None
    num4 := numbers[:]
    fmt.Println("\nOnly end")
    fmt.Printf("num1=%v\n", num4)
    fmt.Printf("length=%d\n", len(num4))
    fmt.Printf("capacity=%d\n", cap(num4))
}

输出

Both start and end
num1=[3 4]
length=2
capacity=3

Only start
num1=[3 4 5]
length=3
capacity=3

Only end
num1=[1 2 3]
length=3
capacity=5

Only end
num1=[1 2 3 4 5]
length=5
capacity=5

请注意在上述示例中,

  • 新创建切片的长度 = (结束开始)

  • 新创建切片的容量 = (数组长度开始)

num1 切片看起来像

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

新创建的切片仍然指向原数组。要检查此更改,可以在数组的任一索引处更改元素,然后重新打印切片。

numbers[3] = 8
fmt.Printf("num1=%v\n", num2)
fmt.Printf("num3=%v\n", num3)
fmt.Printf("num4=%v\n", num4)

这是输出:

num1=[3 8 5]
num3=[1 2 3 8]
num4=[1 2 3 8 5]

这证明每个新切片仍然指向原始数组。

从切片创建切片

我们讨论的从数组重新切片的内容在这里同样适用。请看下面的示例,它说明了同样的事情。

package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5}

    //Both start and end
    num1 := numbers[2:4]
    fmt.Println("Both start and end")
    fmt.Printf("num1=%v\n", num1)
    fmt.Printf("length=%d\n", len(num1))
    fmt.Printf("capacity=%d\n", cap(num1))

    //Only start
    num2 := numbers[2:]
    fmt.Println("\nOnly start")
    fmt.Printf("num1=%v\n", num2)
    fmt.Printf("length=%d\n", len(num2))
    fmt.Printf("capacity=%d\n", cap(num2))

    //Only end
    num3 := numbers[:3]
    fmt.Println("\nOnly end")
    fmt.Printf("num1=%v\n", num3)
    fmt.Printf("length=%d\n", len(num3))
    fmt.Printf("capacity=%d\n", cap(num3))

    //None
    num4 := numbers[:]
    fmt.Println("\nOnly end")
    fmt.Printf("num1=%v\n", num4)
    fmt.Printf("length=%d\n", len(num4))
    fmt.Printf("capacity=%d\n", cap(num4))
}

输出

Both start and end
num1=[3 4]
length=2
capacity=3

Only start
num1=[3 4 5]
length=3
capacity=3

Only end
num1=[1 2 3]
length=3
capacity=5

Only end
num1=[1 2 3 4 5]
length=5
capacity=5

在这里,新创建的切片也指向原切片所引用的同一基础数组。要检查此更改,可以在原切片的任一索引处更改元素,然后重新打印所有新创建的切片。

numbers[3] = 8
fmt.Printf("num1=%v\n", num2)
fmt.Printf("num3=%v\n", num3)
fmt.Printf("num4=%v\n", num4)

这是输出:

num1=[3 8 5]
num3=[1 2 3 8]
num4=[1 2 3 8 5]

使用 make 函数

make 是 Go 提供的一个内置函数,也可以用来创建切片。以下是 make 函数的签名。

func make([]{type}, length, capacity int) []{type}

在使用 make 函数创建切片时,容量是一个可选参数。当省略容量时,切片的容量等于指定的长度。当使用 make 函数时,后台 Go 会分配一个等于容量的数组。所有分配的数组元素都初始化为该类型的默认零值。让我们看一个说明这一点的程序。

package main

import "fmt"

func main() {
    numbers := make([]int, 3, 5)
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //With capacity ommited
    numbers = make([]int, 3)
    fmt.Println("\nCapacity Ommited")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[0 0 0]
length=3
capacity=5

Capacity Ommited
numbers=[0 0 0]
length=3
capacity=3

使用新函数

new是 Go 提供的一个内置函数,也可以用来创建切片。这不是一种非常流行的创建切片的方法,因为make在功能上更灵活。它通常不被使用,使用new函数返回一个指向 nil 切片的指针。让我们看一个例子。在下面的例子中,我们使用解引用操作符‘*’,因为new函数返回一个指向 nil 切片的指针。

package main

import "fmt"

func main() {
    numbers := new([]int)
    fmt.Printf("numbers=%v\n", *numbers)
    fmt.Printf("length=%d\n", len(*numbers))
    fmt.Printf("capacity=%d\n", cap(*numbers))
}

输出

numbers=[]
length=0
capacity=0

长度与容量

在继续之前,让我们强调理解长度和容量的注意事项。让我们创建一个容量大于长度的简单切片。

numbers := make([]int, 3, 5)
  • 访问切片超出其长度会导致运行时错误“索引超出范围”。即使访问的索引在容量内也无关紧要。因此,下面的行将导致运行时错误。
numbers[4] = 5
  • 切片的长度可以通过重新切片增加到其容量。因此,下面的重新切片将长度从 3 增加到 5。
numbers = numbers[0:5]
  • 切片的长度也可以通过重新切片来减少。所以下面的重新切片将长度从 3 减少到 2。
numbers = numbers[0:2]
  • 拥有容量的好处是可以在初始化期间预先分配容量大小的数组。这是一个性能提升,因为如果需要在此数组中包含更多元素,则已经为它们分配了空间。

让我们看一个说明上述要点的程序。

package main

import "fmt"

func main() {
    numbers := make([]int, 3, 5)
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //This line will cause a runtime error index out of range [4] with length 3
    //numbers[4] = 5

    //Increasing the length from 3 to 5
    numbers = numbers[0:5]
    fmt.Println("\nIncreasing length from 3 to 5")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //Decresing the length from 3 to 2
    numbers = numbers[0:2]
    fmt.Println("\nDecreasing length from 3 to 2")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[0 0 0]
length=3
capacity=5

Increasing length from 3 to 5
numbers=[0 0 0 0 0]
length=5
capacity=5

Decreasing length from 3 to 2
numbers=[0 0]
length=2
capacity=5

访问和修改切片元素

可以通过指定索引访问切片元素。切片元素也可以通过索引分配一个新值。此外,请注意,任何对基础数组的更改都会在切片中反映出来,正如我们上面看到的。让我们看一个小例子,展示访问和修改。

package main

import "fmt"

func main() {
    array := [5]int{1, 2, 3, 4, 5}
    slice := array[:]

    //Modifying the slice
    slice[1] = 7
    fmt.Println("Modifying Slice")
    fmt.Printf("Array=%v\n", array)
    fmt.Printf("Slice=%v\n", slice)

    //Modifying the array. Would reflect back in slice too
    array[1] = 2
    fmt.Println("\nModifying Underlying Array")
    fmt.Printf("Array=%v\n", array)
    fmt.Printf("Slice=%v\n", slice)
}

输出

Modifying Slice
Array=[1 7 3 4 5]
Slice=[1 7 3 4 5]

Modifying Underlying Array
Array=[1 2 3 4 5]
Slice=[1 2 3 4 5]

不同的切片迭代方式

数组可以通过以下方式迭代:

  • 使用 for 循环

  • 使用 for-range 循环

让我们看一个代码示例。

package main

import "fmt"

func main() {
    letters := []string{"a", "b", "c"}
    //Using for loop
    fmt.Println("Using for loop")
    len := len(letters)
    for i := 0; i < len; i++ {
        fmt.Println(letters[i])
    }

    //Using for-range operator
    fmt.Println("\nUsing for-range loop")
    for i, letter := range letters {
        fmt.Printf("%d %s\n", i, letter)
    }
}

输出

Using for loop
a
b
c

Using for-range loop
0 a
1 b
2 c

向切片追加元素

Go 的builtin包提供了一个append函数,可以在切片末尾追加元素。下面是这个函数的签名。

func append(slice []Type, elems ...Type) []Type

第一个参数是切片本身。第二个是可变数量的参数。

elems ...Type

'...'运算符是可变语法。因此,基本上...Type意味着追加函数可以接受可变数量的类型为Type的参数。以下是使用该函数的方法。在下面的代码中,我们将 4 追加到一个有两个元素的切片中。它在末尾追加并返回原始切片。这就是为什么我们将结果再次收集到numbers变量中的原因。将结果分配给其他变量也是可以的。

numbers := []int{1,2}
numbers = append(numbers, 4) //Slice will become [1, 2, 4]

追加多个元素也是可以的,因为第二个参数是可变参数。

numbers := []int{1,2}
numbers = append(numbers, 3, 4, 5) //Slice will become [1, 2, 3, 4, 5]

此函数在后台会增加切片的长度和容量。有两种情况。

  • 当切片长度小于容量时。

在这种情况下,使用追加函数时,切片的长度将增加 1,而容量保持不变。让我们看一个例子。

package main

import "fmt"

func main() {
    numbers := make([]int, 3, 5)
    numbers[0] = 1
    numbers[1] = 2
    numbers[2] = 3
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //Append number 4
    numbers = append(numbers, 4)
    fmt.Println("\nAppend Number 4")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //Append number 5
    numbers = append(numbers, 4)
    fmt.Println("\nAppend Number 5")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[1 2 3]
length=3
capacity=5

Append Number 4
numbers=[1 2 3 4]
length=4
capacity=5

Append Number 5
numbers=[1 2 3 4 4]
length=5
capacity=5

在所有情况下,容量都保持不变,都是 5,而长度增加了 1。

  • 当切片长度大于容量时。

在这种情况下,由于没有更多的容量,所以无法容纳新的元素。因此在底层将分配一个容量翻倍的数组。当前切片指向的数组将被复制到这个新数组。现在切片将开始指向这个新数组。因此容量将翻倍,长度将增加 1。让我们看一个例子。

package main

import "fmt"

func main() {
    numbers := make([]int, 3, 3)
    numbers[0] = 1
    numbers[1] = 2
    numbers[2] = 3

    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))

    //Append number 4
    numbers = append(numbers, 4)
    fmt.Println("\nAppend Number 4")
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[1 2 3]
length=3
capacity=3

Append Number 4
numbers=[1 2 3 4]
length=4
capacity=6

请注意上面的例子中,容量是翻倍的。

也可以将一个切片追加到另一个切片。以下是该操作的格式。

res := append(slice1, slice2...)

注意第二个切片后的'...''...'是运算符,表示该参数是可变参数。这意味着在运行时,slice2 将扩展为作为多个参数传递给追加函数的各个元素。

package main

import "fmt"

func main() {
    numbers1 := []int{1, 2}
    numbers2 := []int{3, 4}
    numbers := append(numbers1, numbers2...)
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[1 2 3 4]
length=4
capacity=4

复制切片

Go 的builtin包提供了copy函数,可以用于复制切片。以下是该函数的签名。它接受两个切片dstsrc,并将数据从src复制到dst。它返回复制的元素数量。

func copy(dst, src []Type) int

使用复制函数时需要考虑两种情况:

  • 如果src的长度大于dst的长度,则复制的元素数量为dst的长度。

  • 如果dst的长度大于src的长度,则复制的元素数量为src的长度。

基本上,复制的元素数量是(src, dst)长度的最小值。

还需注意,一旦复制完成,对dst的任何更改将不会反映在src中,反之亦然。让我们看一个例子。

package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, 5)

    numberOfElementsCopied := copy(dst, src)
    fmt.Printf("Number Of Elements Copied: %d\n", numberOfElementsCopied)
    fmt.Printf("dst: %v\n", dst)
    fmt.Printf("src: %v\n", src)

    //After changing numbers2
    dst[0] = 10
    fmt.Println("\nAfter changing dst")
    fmt.Printf("dst: %v\n", dst)
    fmt.Printf("src: %v\n", src)
}

输出

Number Of Elements Copied: 5
dst: [1 2 3 4 5]
src: [1 2 3 4 5]

After changing dst
dst: [10 2 3 4 5]
src: [1 2 3 4 5]

Nil 切片

切片的默认零值是 nil。nil 切片的长度和容量都是零。虽然将元素追加到 nil 切片也是可以的。让我们看一个例子。

package main

import "fmt"

func main() {
    var numbers []int
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
    numbers = append(numbers, 1)
    fmt.Printf("numbers=%v\n", numbers)
    fmt.Printf("length=%d\n", len(numbers))
    fmt.Printf("capacity=%d\n", cap(numbers))
}

输出

numbers=[]
length=0
capacity=0
numbers=[1]
length=1
capacity=1

多维切片

由于多维数组是数组的数组,因此多维切片是切片的切片。为了理解这一点,让我们先看看切片的定义。

切片头中的数据字段是指向基础数组的指针。 对于一维切片,我们有以下声明。

oneDSlice := make([]int, 2)

声明一个二维切片的声明应该是。

twoDSlice = make([][]int, 2)

上述声明意味着我们想创建一个切片,包含 2 个切片。仔细理解这一点。但等一下,这里我们没有指定第二维,意味着每个内部 2 个切片的长度。在切片的情况下,每个内部切片必须像下面那样显式初始化。

for i := range twoDSlice {
    twoDSlice[i] = make([]int, 3)
}

因此,通过原始切片上的范围,我们使用 make 指定每个 2 个切片的长度。 下面是另一种方法,但指定了切片元素。

var twoDSlice = make([][]int, 2)
twoDSlice[0] = []int{1, 2, 3}
twoDSlice[1] = []int{4, 5, 6}

基本上,通过上述声明,我们创建了一个 2*3 维的切片,这是一个二维切片。相同的想法可以扩展到二维、三维等。

上述两点的完整工作示例

package main

import "fmt"

func main() {
    twoDSlice1 := make([][]int, 3)
    for i := range twoDSlice1 {
        twoDSlice1[i] = make([]int, 3)
    }
    fmt.Printf("Number of rows in slice: %d\n", len(twoDSlice1))
    fmt.Printf("Number of columns in arsliceray: %d\n", len(twoDSlice1[0]))
    fmt.Printf("Total number of elements in slice: %d\n", len(twoDSlice1)*len(twoDSlice1[0]))
    fmt.Println("First Slice")
    for _, row := range twoDSlice1 {
        for _, val := range row {
            fmt.Println(val)
        }
    }
    twoDSlice2 := make([][]int, 2)
    twoDSlice2[0] = []int{1, 2, 3}
    twoDSlice2[1] = []int{4, 5, 6}
    fmt.Println()
    fmt.Printf("Number of rows in slice: %d\n", len(twoDSlice2))
    fmt.Printf("Number of columns in arsliceray: %d\n", len(twoDSlice2[0]))
    fmt.Printf("Total number of elements in slice: %d\n", len(twoDSlice2)*len(twoDSlice2[0]))
    fmt.Println("Second Slice")
    for _, row := range twoDSlice2 {
        for _, val := range row {
            fmt.Println(val)
        }
    }
}

输出

Number of rows in slice: 2
Number of columns in arsliceray: 3
Total number of elements in slice: 6
First Slice
0
0
0
0
0
0

Number of rows in slice: 2
Number of columns in arsliceray: 3
Total number of elements in slice: 6
Second Slice
1
2
3
4
5
6

我们在上面提到我们正在创建一个 2*3 维的二维切片。 也就是说,您可能会想到的问题是,内部切片是否可以有不同的长度。 是的,这是可能的。 与内部数组具有相同长度的数组不同,在切片的情况下,由于我们单独初始化每个内部切片,因此可以拥有不同长度的内部切片。

让我们看看一个示例。

package main

import "fmt"

func main() {
    twoDSlice := make([][]int, 2)
    twoDSlice[0] = []int{1, 2, 3}
    twoDSlice[1] = []int{4, 5}

    fmt.Printf("Number of rows in slice: %d\n", len(twoDSlice))
    fmt.Printf("Len of first row: %d\n", len(twoDSlice[0]))
    fmt.Printf("Len of second row: %d\n", len(twoDSlice[1]))
    fmt.Println("Traversing slice")
    for _, row := range twoDSlice {
        for _, val := range row {
            fmt.Println(val)
        }
    }
}

输出

Number of rows in slice: 2
Len of first row: 3
Len of second row: 2
Traversing slice
1
2
3
4
5

让我们看看一个三维切片的小例子。在下面的程序中,我们正在创建一个 223 维的切片。

package main

import "fmt"

func main() {
    sample := make([][][]int, 2)
    for i := range sample {
        sample[i] = make([][]int, 2)
        for j := range sample[i] {
            sample[i][j] = make([]int, 3)
        }
    }

    fmt.Printf("Length of first dimension: %d\n", len(sample))
    fmt.Printf("Length of second dimension: %d\n", len(sample[0]))
    fmt.Printf("Length of third dimension: %d\n", len(sample[0][0]))
    fmt.Printf("Overall Dimension of the slice: %d*%d*%d\n", len(sample), len(sample[0]), len(sample[0][0]))
    fmt.Printf("Total number of elements in slice: %d\n", len(sample)*len(sample[0])*len(sample[0][0]))
    for _, first := range sample {
        for _, second := range first {
            for _, value := range second {
                fmt.Println(value)
            }
        }
    }
}

输出

Length of first dimension: 2
Length of second dimension: 2
Length of third dimension: 3
Overall Dimension of the slice: 2*2*3
Total number of elements in slice: 12
0
0
0
0
0
0
0
0
0
0
0
0

结论

这就是 golang 中的切片。希望您喜欢这篇文章。请在评论中分享反馈/改进/错误。

下一个教程映射

前一个教程数组

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