Наиболее эффективный способ для конкатенации - использовать тип strings.Builder. Этот тип предоставляет более оптимизированные по выделению памяти методы для построения строк.
A Builder is used to efficiently build a string using Write methods. It minimizes memory copying. The zero value is ready to use.
package main
import (
"fmt"
"strings"
)
func main() {
var sb strings.Builder
sb.WriteString("Hello, ")
sb.WriteString("world!")
sb.WriteString(" How are you?")
result := sb.String() // Получить итоговую строку
fmt.Println(result)
}
Интерфейсы в Go — это абстрактные типы, которые определяют набор методов, которые должны быть реализованы конкретным типом. Интерфейсы используются для определения и обеспечения контракта, которому должен соответствовать тип, но без предписания конкретной реализации этих методов:
type Animal interface {
Speak() string
}
type Dog struct {}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
animals := []Animal{Dog{}, Cat{}}
for _, animal := range animals {
fmt.Println(animal.Speak())
}
}
sync.Mutex и sync.RWMutex служат для управления доступом к общим ресурсам, но они используются в различных сценариях и работают немного по-разному.
Mutex:
sync.Mutex
предоставляет базовый механизм блокировки, который гарантирует, что только одна горутина может получить доступ к ресурсам в данный момент времени. Если один горутина захватила Mutex
, любая другая горутина, будет блокироваться до тех пор, пока он не будет освобождён.
import "sync"
var mutex sync.Mutex
var sharedResource int
func updateResource() {
mutex.Lock() // Блокировка доступа к sharedResource
sharedResource = sharedResource + 1
mutex.Unlock() // Освобождение доступа
}
RWMutex:
sync.RWMutex
предоставляет более сложный механизм блокировки для ситуаций, когда у вас есть множество горутин, которые только читают данные, и меньшее число горутин, которые обновляют данные. RWMutex
имеет два типа блокировок: блокировку для чтения (RLock
) и блокировку для записи (Lock
).
RLock (Read Lock)
: Блокирует ресурс для чтения. Множество горутин могут одновременно удерживать блокировку для чтения, при условии, что никакие горутины не удерживают блокировку для записи. Это улучшает производительность в сценариях с большим количеством операций чтения и меньшим количеством операций записи.Lock (Write Lock)
: Блокирует ресурс для записи. Только одна горутина может удерживать блокировку для записи, и никакие другие не могут удерживать ни блокировку для чтения, ни блокировку для записи.
import "sync"
var rwMutex sync.RWMutex
var sharedResource int
func readResource() int {
rwMutex.RLock() // Блокировка для чтения
value := sharedResource
rwMutex.RUnlock() // Освобождение
return value
}
func writeResource(value int) {
rwMutex.Lock() // Блокировка для записи
sharedResource = value
rwMutex.Unlock() // Освобождение
}
Не буферизированные каналы не имеет внутреннего хранилища для значений. Отправка в не буферизированный канал блокируется до тех пор, пока другая горутина не выполнит операцию получения из этого канала, и наоборот: операция получения блокируется до тех пор, пока не будет выполнена операция отправки.
h := make(chan int) // Создание не буферизированного канала
go func() {
// Эта операция блокируется, пока значение не будет получено.
ch <- 42
}()
// После того как значение будет получено, горутина продолжит работу.
value := <-ch
fmt.Println(value)
Буферизированные каналы имеют внутреннюю очередь определённого размера. Операция отправки в буферизированный канал блокируется только тогда, когда буфер заполнен. Аналогично, операция получения блокируется только когда буфер пуст.
h := make(chan int, 2) // Создание буферизированного канала с размером буфера 2
// Операция отправки не блокируется, потому что есть свободное место в буфере.
ch <- 42
ch <- 27
// Третья операция отправки будет блокироваться, пока не освободится место в буфере.
// go func() {
// ch <- 100
// }()
value := <-ch // Получение значения из канала
fmt.Println(value) // Выводит 42
Cтруктура struct{}{}
не содержит никаких полей. Она называется пустой структурой и имеет размер 0 байт. Это может быть проверено с помощью функции unsafe.Sizeof
, которая возвращает размер в байтах значения, переданного в качестве аргумента.
package main
import (
"fmt"
"unsafe"
)
func main() {
var emptyStruct struct{}
fmt.Println(unsafe.Sizeof(emptyStruct)) // Выведет 0
}
В Go нет перегрузки методов или операторов, как это есть в некоторых других языках программирования, таких как C++ или Java. В Go каждый метод должен иметь уникальное имя в рамках определённого типа. Это означает, что вы не можете иметь два метода с одним и тем же именем, даже если они принимают разные параметры.
Также не поддерживается перегрузка операторов. В языке есть фиксированный набор операторов с заранее определёнными операциями для встроенных типов, и вы не можете изменить или расширить эти операции для пользовательских типов. Это также способствует простоте и предсказуемости языка.
m[0]=1
m[1]=124
m[2]=281
Элементы map[int]int
выводятся в случайном порядке. Это связано с тем, что Go использует хэш-таблицу для реализации map, и порядок элементов в хэш-таблице не определен.
В Go, make
и new
используются для аллокации памяти, но они служат разным целям и работают по-разному.
new: Функция new(T)
используется для аллокации памяти для значения типа T, которое инициализировано нулевым значением типа T и возвращает указатель на него. Нулевое значение зависит от типа: для чисел это будет 0, для булевых значений false
, для указателей, слайсов, мап, каналов и функций это будет nil
. new
можно использовать для любого типа данных.
make: используется исключительно для инициализации слайсов, мап и каналов и возвращает инициализированный (не нулевой) экземпляр типа, а не указатель. make
выделяет и инициализирует эти структуры данных, а также настраивает их внутренние свойства, такие как длина и емкость в случае слайса.
Разница между make и new:
new
возвращает указатель на нулевое значение типа, тогда какmake
возвращает инициализированный экземпляр структуры данных.new
работает с любым типом, возвращая указатель,make
же применима только к слайсам, мапам и каналам.make
обеспечивает дополнительную инициализацию, важную для работы внутренних структур данных, в то время какnew
просто выделяет память.
Пример использования make
и new
:
b := new([]int) // b - указатель на нулевой слайс, то есть на nil
c := make([]int, 0) // c - инициализированный пустой слайс с длиной и емкостью 0
fmt.Println(b) // Выведет &[], указатель на нулевой слайс
fmt.Println(c) // Выведет [], инициализированный пустой слайс
В этом примере b является указателем на слайс, который еще не был инициализирован (он указывает на nil), в то время как c является инициализированным слайсом с длиной и емкостью, равными 0.
Для определения переменной типа slice или map в Go существует несколько способов. Вот основные из них:
s := []int{1, 2, 3}
s := make([]int, 3) // Слайс из трех нулевых элементов (длина 3, емкость 3)
s := make([]int, 3, 5) // Длина 3, емкость 5
var s []int // s == nil
s := []int{} // или s := make([]int, 0)
s := new([]int) // s - указатель на слайс, *s == nil
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // Слайс, содержащий элементы 2, 3, 4
m := map[string]int{"one": 1, "two": 2}
m := make(map[string]int) // Создает пустой map
var m map[string]int // m == nil
func update(p *int) {
b := 2
p = &b
}
func main() {
var (
a = 1
p = &a
)
fmt.Println(*p) // 1
update(p)
fmt.Println(*p) // 1
}
- В функции main объявляется переменная a со значением 1.
- Создается указатель p, который указывает на переменную a.
- С помощью
fmt.Println(*p)
выводится значение, на которое указывает p, то есть 1. - Вызывается функция update, в которую передается копия указателя p.
- В функции update переменная
b
иннициализируется значением 2. - Внутри update переменной
p
присваивается адрес переменнойb
. Это изменение локально для функции update, так как в Go аргументы передаются по значению, и это означает, что p в update является копией исходного указателяp
из main. Изменение локальногоp
в функции update не влияет на значениеp
в функции main. - После завершения функции update программа возвращается в функцию main, и снова вызывается
fmt.Println(*p)
. Поскольку функция update не изменила значениеa
и не изменила исходный указательp
в функции main,*p
все еще равно1
.
Это происходит потому, что изменения в функции update затрагивают только локальную копию указателя, а не исходное значение, на которое указывает p в функции main.
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg sync.WaitGroup, i int) {
fmt.Println(i)
wg.Done()
}(wg, i)
}
wg.Wait()
fmt.Println("exit")
}
Вывод:
4
2
1
0
3
fatal error: all goroutines are asleep - deadlock!
При работе программы получили deadlock, из-за того, что передаём копию WaitGroup
в горутину, а не ссылку на оригинальный WaitGroup
. В Go sync.WaitGroup
использует внутренний счетчик, чтобы отслеживать количество ожидающих горутин.
Когда передаём wg
по значению, каждая горутина получает свою копию WaitGroup
, и вызов Done()
уменьшает счетчик в этой копии, а не в оригинальном WaitGroup
, с которым вызывается Wait()
в главной горутине.
Чтобы исправить эту проблему, нужно передавать wg
по ссылке, а не по значению. Вот исправленный вариант кода:
func main() {
wg := &sync.WaitGroup{} // Используем указатель на WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup, i int) { // Передаем указатель
defer wg.Done() // Используем defer для гарантии вызова Done() даже если возникнет паника
fmt.Println(i)
}(wg, i) // Передаем wg как указатель
}
wg.Wait()
fmt.Println("exit")
}
func main() {
n := 0
if true {
n := 1
n++
}
fmt.Println(n) // 0
}
В данном коде выводит 0
, потому что в блоке if true { ... }
создается новая локальная переменная n
вместо использования уже существующей переменной n
, объявленной в функции main
.
Внешняя переменная n
, объявленная в функции main
до блока if, не изменяется и остается равной 0
. Именно ее значение и выводится в конце программы.
Чтобы исправить эту ошибку, нужно использовать оператор присваивания =
вместо :=
в блоке if
.
func someAction(v []int8, b int8) {
v[0] = 100
v = append(v, b)
}
func main() {
var a = []int8{1, 2, 3, 4, 5}
someAction(a, 6)
fmt.Println(a) // [100 2 3 4 5]
}
Вывод будет равен [100 2 3 4 5]
.
В Go, слайсы передаются в функции по значению
, но это значение
включает в себя ссылку на массив, на который ссылается переданный слайс. Все изменения, внесенные в элементы слайса внутри функции, отразятся в исходном слайсе в вызывающей функции.
При выполнении операции append, если в исходном массиве достаточно места для нового элемента, append изменит исходный массив. Если нет – будет создан новый массив, и v
будет теперь указывать на этот новый массив. В данном случае, поскольку массив a
может вместить еще элементы, append
не создаст новый массив, и изменение будет внесено в исходный массив.
Для решения данной проблемы нужно, чтобы функция someAction
возвращала новый слайс, а не изменяла предоставленный:
package main
import (
"fmt"
)
func someAction(v []int8, b int8) []int8 {
v[0] = 100
v = append(v, b)
return v
}
func main() {
var a = []int8{1, 2, 3, 4, 5}
a = someAction(a, 6)
fmt.Println(a) // Вывод: [100 2 3 4 5 6]
}
В этом случае изменения внесены в копию слайса, а затем эта измененная копия возвращается и присваивается обратно переменной a
в функции main
func main() {
slice := []string{"a", "a"}
func(slice []string) {
slice = append(slice, "a")
slice[0] = "b"
slice[1] = "b"
fmt.Print(slice)
}(slice)
fmt.Print(slice) // Вывод: [b b a][a a]
}
Программа выводит следующее:
[b b a][a a]
- Создается слайс slice с двумя элементами.
- Затем объявляется и немедленно вызывается анонимная функция, которой в качестве аргумента передается слайс
slice
. Переменнаяslice
внутри функции является копией ссылки на массив, то есть изменения в переменнуюslice
внутри функции не отражаются на внешнемslice
. - Внутри анонимной функции к слайсу добавляется еще один элемент "a" с помощью append. Если в исходном слайсе было достаточно места для еще одного элемента (capacity >= 3), то изменения будут внесены в исходный массив и он будет виден и снаружи функции. Если же места не хватило, append создаст новый массив и скопирует туда элементы из старого, увеличив его capacity. В данном случае исходный слайс имел длину 2 и capacity, скорее всего, тоже 2 (так как не указано явно и Go может выделить capacity ровно под количество элементов), поэтому append выделит новый массив.
- Далее внутри анонимной функции элементы с индексами 0 и 1 меняют свои значения на "b". Так как возможно было создание нового массива на предыдущем шаге, изменения происходят в новом массиве, но если бы append не привел к выделению нового массива, изменения отразились бы и на внешнем слайсе.
- Анонимная функция выводит измененный слайс, который теперь содержит
["b", "b", "a"]
. - После выполнения анонимной функции, основная функция main выводит исходный слайс
slice
, который не изменился в результате работы анонимной функции и содержит["a", "a"]
, так как изменения в анонимной функции либо происходили в новом массиве (если был выделен из-за недостатка capacity), либо не затрагивали внешнюю переменную slice.
Таким образом, сначала выводится измененный в анонимной функции слайс [b b a]
, а затем не измененный исходный слайс [a a]
.