├── tasks ├── 03 │ ├── pr │ │ ├── testdata │ │ │ ├── multiple_files │ │ │ │ ├── b.txt │ │ │ │ ├── a.txt │ │ │ │ ├── c.txt │ │ │ │ └── expected.txt │ │ │ ├── 01 │ │ │ │ ├── simple.txt │ │ │ │ └── simple.txt.output │ │ │ ├── 03 │ │ │ │ ├── add_newlines.txt │ │ │ │ └── add_newlines.txt.output │ │ │ └── 02 │ │ │ │ ├── multiple_pages.txt │ │ │ │ └── multiple_pages.txt.output │ │ ├── info.yaml │ │ ├── cmd │ │ │ └── gopr │ │ │ │ └── main.go │ │ ├── README.md │ │ └── pr_test.go │ ├── ini │ │ ├── info.yaml │ │ ├── parse.go │ │ ├── README.md │ │ └── parse_test.go │ ├── polish │ │ ├── info.yaml │ │ ├── calculate.go │ │ ├── README.md │ │ └── calculate_test.go │ └── courses │ │ ├── info.yaml │ │ ├── applications.go │ │ ├── README.md │ │ └── applications_test.go ├── 01 │ ├── hello │ │ ├── info.yaml │ │ ├── hello.go │ │ └── hello_test.go │ └── sum │ │ ├── info.yaml │ │ ├── sum.go │ │ └── sum_test.go ├── 02 │ ├── caesar │ │ ├── info.yaml │ │ ├── caesar.go │ │ ├── README.md │ │ └── caesar_test.go │ ├── lcp │ │ ├── info.yaml │ │ ├── lcp.go │ │ ├── README.md │ │ └── lcp_test.go │ ├── rle │ │ ├── info.yaml │ │ ├── rle.go │ │ ├── rle_test.go │ │ └── README.md │ └── reverse │ │ ├── info.yaml │ │ ├── reverse_string.go │ │ ├── README.md │ │ └── reverse_string_test.go ├── 04 │ ├── rw │ │ ├── info.yaml │ │ ├── writers.go │ │ ├── readers.go │ │ ├── rw_test.go │ │ └── README.md │ ├── jsoncensor │ │ ├── info.yaml │ │ ├── censor.go │ │ ├── README.md │ │ └── censor_test.go │ └── retries │ │ ├── info.yaml │ │ ├── api.go │ │ ├── README.md │ │ └── api_test.go ├── 05 │ ├── dining │ │ ├── info.yaml │ │ ├── README.md │ │ ├── fork.go │ │ └── fork_test.go │ ├── downloader │ │ ├── info.yaml │ │ ├── downloader.go │ │ ├── README.md │ │ └── downloader_test.go │ ├── datatransfer │ │ ├── info.yaml │ │ ├── transfer.go │ │ ├── README.md │ │ └── transfer_test.go │ └── philosophers │ │ ├── info.yaml │ │ ├── README.md │ │ ├── problem_test.go │ │ └── problem.go └── 06 │ ├── formvalues │ ├── info.yaml │ ├── marshal.go │ ├── README.md │ └── marshal_test.go │ ├── mockhelper │ ├── info.yaml │ ├── mock_helper.go │ ├── README.md │ └── mock_helper_test.go │ └── genericutils │ ├── info.yaml │ ├── utils.go │ ├── README.md │ └── utils_test.go ├── lectures ├── 01 │ ├── code │ │ ├── hello │ │ │ └── hello.go │ │ ├── echo3 │ │ │ └── echo.go │ │ ├── echo2 │ │ │ └── echo.go │ │ ├── echo │ │ │ └── echo.go │ │ ├── dup │ │ │ └── main.go │ │ ├── fetch │ │ │ └── main.go │ │ ├── dup3 │ │ │ └── main.go │ │ ├── dup2 │ │ │ └── main.go │ │ ├── fetchall │ │ │ └── main.go │ │ └── downloader │ │ │ └── main.go │ ├── README.md │ └── lecture.slide ├── 02 │ ├── README.md │ ├── code │ │ └── tempconv │ │ │ └── tempconv.go │ └── lecture.slide ├── 09 │ ├── README.md │ ├── code │ │ ├── constraints │ │ │ └── main.go │ │ ├── inference │ │ │ └── main.go │ │ └── type_parameters │ │ │ └── main.go │ └── lecture.slide ├── 07 │ ├── README.md │ ├── code │ │ └── palindrome │ │ │ ├── palindrome.go │ │ │ └── palindrome_test.go │ └── lecture.slide ├── 08 │ ├── README.md │ ├── code │ │ ├── test │ │ │ └── main.go │ │ ├── valueset │ │ │ └── main.go │ │ ├── value │ │ │ └── main.go │ │ └── display │ │ │ └── main.go │ └── lecture.slide ├── 04 │ ├── README.md │ ├── code │ │ └── interfaces │ │ │ └── main.go │ └── lecture.slide ├── 05 │ ├── README.md │ ├── code │ │ └── clock │ │ │ └── main.go │ └── lecture.slide ├── 06 │ ├── README.md │ ├── code │ │ ├── cancel │ │ │ └── main.go │ │ ├── auth │ │ │ └── main.go │ │ └── cache │ │ │ └── main.go │ └── lecture.slide └── 03 │ ├── README.md │ └── lecture.slide ├── .gitignore ├── go.mod ├── utils └── testutils │ ├── tempfile.go │ └── binary.go ├── README.md ├── LICENSE └── go.sum /tasks/03/pr/testdata/multiple_files/b.txt: -------------------------------------------------------------------------------- 1 | Hello! -------------------------------------------------------------------------------- /tasks/03/pr/testdata/01/simple.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d -------------------------------------------------------------------------------- /tasks/01/hello/info.yaml: -------------------------------------------------------------------------------- 1 | score: 1 2 | deadline: 2025-02-27 3 | -------------------------------------------------------------------------------- /tasks/01/sum/info.yaml: -------------------------------------------------------------------------------- 1 | score: 1 2 | deadline: 2025-02-27 3 | -------------------------------------------------------------------------------- /tasks/02/caesar/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/02/lcp/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/02/rle/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/03/ini/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/03/polish/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/03/pr/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/04/rw/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-04-03 3 | -------------------------------------------------------------------------------- /tasks/05/dining/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-04-17 3 | -------------------------------------------------------------------------------- /tasks/02/reverse/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/03/courses/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-03-13 3 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/03/add_newlines.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e -------------------------------------------------------------------------------- /tasks/04/jsoncensor/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-04-03 3 | -------------------------------------------------------------------------------- /tasks/04/retries/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-04-03 3 | -------------------------------------------------------------------------------- /tasks/05/downloader/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-04-17 3 | -------------------------------------------------------------------------------- /tasks/06/formvalues/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-05-22 3 | -------------------------------------------------------------------------------- /tasks/06/mockhelper/info.yaml: -------------------------------------------------------------------------------- 1 | score: 3 2 | deadline: 2025-05-22 3 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/multiple_files/a.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | f -------------------------------------------------------------------------------- /tasks/05/datatransfer/info.yaml: -------------------------------------------------------------------------------- 1 | score: 4 2 | deadline: 2025-04-17 3 | -------------------------------------------------------------------------------- /tasks/05/philosophers/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-04-17 3 | -------------------------------------------------------------------------------- /tasks/06/genericutils/info.yaml: -------------------------------------------------------------------------------- 1 | score: 2 2 | deadline: 2025-05-22 3 | -------------------------------------------------------------------------------- /tasks/03/pr/cmd/gopr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | } 5 | -------------------------------------------------------------------------------- /tasks/02/rle/rle.go: -------------------------------------------------------------------------------- 1 | package rle 2 | 3 | func RLECompress(input string) string { 4 | panic("implement me") 5 | } 6 | -------------------------------------------------------------------------------- /tasks/01/sum/sum.go: -------------------------------------------------------------------------------- 1 | package sum 2 | 3 | func Sum(values []int) int { 4 | // Your implementation 5 | return 0 6 | } 7 | -------------------------------------------------------------------------------- /tasks/02/lcp/lcp.go: -------------------------------------------------------------------------------- 1 | package lcp 2 | 3 | func LongestCommonPrefix(l, r string) string { 4 | panic("implement me") 5 | } 6 | -------------------------------------------------------------------------------- /lectures/01/code/hello/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello, world!") 7 | } 8 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/01/simple.txt.output: -------------------------------------------------------------------------------- 1 | testdata/01/simple.txt Page: 0 2 | 3 | a 4 | b 5 | c 6 | d 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tasks/02/reverse/reverse_string.go: -------------------------------------------------------------------------------- 1 | package reverse 2 | 3 | func ReverseString(input string) string { 4 | panic("implement me") 5 | } 6 | -------------------------------------------------------------------------------- /tasks/01/hello/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | const ( 4 | helloMessage = "Hello, world!" 5 | ) 6 | 7 | func Hello() string { 8 | return helloMessage 9 | } -------------------------------------------------------------------------------- /lectures/02/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 2. Базовые конструкции языка 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/02/lecture.slide.html) 4 | -------------------------------------------------------------------------------- /lectures/09/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 8. Generics 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/09/lecture.slide.html) 4 | * [Код](code) 5 | -------------------------------------------------------------------------------- /tasks/04/jsoncensor/censor.go: -------------------------------------------------------------------------------- 1 | package jsoncensor 2 | 3 | func CensorJSON(jsonData []byte, substr string) ([]byte, error) { 4 | panic("implement me") 5 | } 6 | -------------------------------------------------------------------------------- /lectures/07/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 7. Тестирование 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/07/lecture.slide.html) 4 | * [Код](code) 5 | -------------------------------------------------------------------------------- /lectures/08/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 8. Reflection 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/08/lecture.slide.html) 4 | * [Код](code) 5 | -------------------------------------------------------------------------------- /lectures/04/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 4. Defer и Panic. Интерфейсы. Ошибки 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/04/lecture.slide.html) 4 | -------------------------------------------------------------------------------- /lectures/05/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 5. Конкурентность и параллелизм. Горутины. Каналы 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/05/lecture.slide.html) 4 | -------------------------------------------------------------------------------- /lectures/06/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 6. Механизмы синхронизации 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/06/lecture.slide.html) 4 | * [Код с лекции](code/) 5 | -------------------------------------------------------------------------------- /lectures/01/code/echo3/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(strings.Join(os.Args[1:], " ")) 11 | } 12 | -------------------------------------------------------------------------------- /tasks/06/formvalues/marshal.go: -------------------------------------------------------------------------------- 1 | package formvalues 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func Unpack(req *http.Request, ptr interface{}) error { 8 | panic("implement me") 9 | } 10 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/03/add_newlines.txt.output: -------------------------------------------------------------------------------- 1 | testdata/03/add_newlines.txt Page: 0 2 | 3 | a 4 | b 5 | c 6 | d 7 | 8 | 9 | 10 | testdata/03/add_newlines.txt Page: 1 11 | 12 | e 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tasks/03/polish/calculate.go: -------------------------------------------------------------------------------- 1 | package polish 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrInvalidExpression = errors.New("invalid expression") 8 | 9 | func Calculate(expr string) (int, error) { 10 | panic("implement me") 11 | } 12 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/multiple_files/c.txt: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::ios_base::sync_with_stdio(false); 5 | std::cin.tie(nullptr); 6 | 7 | 8 | std::cout << "Hello, World!\n"; 9 | 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /lectures/01/code/echo2/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | s, sep := "", "" 10 | for _, arg := range os.Args[1:] { 11 | s += sep + arg 12 | sep = " " 13 | } 14 | fmt.Println(s) 15 | } 16 | -------------------------------------------------------------------------------- /tasks/02/caesar/caesar.go: -------------------------------------------------------------------------------- 1 | package caesar 2 | 3 | func Cipher(s string, shift int) string { 4 | var result []rune 5 | for _, char := range s { 6 | var newChar = char 7 | result = append(result, newChar) 8 | } 9 | return string(result) 10 | } 11 | -------------------------------------------------------------------------------- /tasks/03/ini/parse.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrFileIsMalformed = errors.New("file is malformed") 8 | 9 | func Parse(fileName string) (map[string]map[string]string, error) { 10 | panic("implement me") 11 | } 12 | -------------------------------------------------------------------------------- /lectures/01/code/echo/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | var s, sep string 10 | for i := 1; i < len(os.Args); i++ { 11 | s += sep + os.Args[i] 12 | sep = " " 13 | } 14 | fmt.Println(s) 15 | } 16 | -------------------------------------------------------------------------------- /lectures/03/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 3. Составные типы. Ошибки 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/03/lecture.slide.html) 4 | 5 | * [50 оттенков Go: ловушки, подводные камни и распространённые ошибки новичков](https://habr.com/ru/company/mailru/blog/314804/) 6 | -------------------------------------------------------------------------------- /lectures/08/code/test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func main() { 9 | var f Foo 10 | 11 | val := reflect.ValueOf(&f) 12 | 13 | val.Elem().Field(0).SetInt(42) 14 | fmt.Println(f) 15 | 16 | } 17 | 18 | type Foo struct { 19 | I int 20 | } 21 | -------------------------------------------------------------------------------- /tasks/01/hello/hello_test.go: -------------------------------------------------------------------------------- 1 | package hello_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | 6 | "github.com/dbeliakov/mipt-golang-course/tasks/01/hello" 7 | 8 | "testing" 9 | ) 10 | 11 | func TestHello(t *testing.T) { 12 | require.Equal(t, "Hello, world!", hello.Hello()) 13 | } -------------------------------------------------------------------------------- /tasks/04/rw/writers.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type HexWriter struct { 8 | } 9 | 10 | func NewHexWriter(w io.Writer) *HexWriter { 11 | return &HexWriter{} 12 | } 13 | 14 | type TeeWriter struct { 15 | } 16 | 17 | func NewTeeWriter(writers ...io.Writer) *TeeWriter { 18 | return &TeeWriter{} 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /lectures/02/code/tempconv/tempconv.go: -------------------------------------------------------------------------------- 1 | package tempconv 2 | 3 | type Celsius float64 4 | type Fahrenheit float64 5 | 6 | const ( 7 | AbsoluteZeroC Celsius = -273.15 8 | FreezingC Celsius = 0 9 | BoilingC Celsius = 100 10 | ) 11 | 12 | func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } 13 | 14 | func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } 15 | -------------------------------------------------------------------------------- /lectures/01/code/dup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | counts := make(map[string]int) 11 | input := bufio.NewScanner(os.Stdin) 12 | for input.Scan() { 13 | counts[input.Text()]++ 14 | } 15 | // NOTE: ignoring potential errors from input.Err() 16 | for line, n := range counts { 17 | if n > 1 { 18 | fmt.Printf("%d\t%s\n", n, line) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lectures/09/code/constraints/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type UInt interface { 6 | uint | uint8 | uint16 | uint32 | uint64 7 | } 8 | 9 | func MaxUInt[T UInt](a, b T) T { 10 | if a < b { 11 | return a 12 | } 13 | return b 14 | } 15 | 16 | func main() { 17 | // var x UInt // interface contains type constraints 18 | fmt.Println(MaxUInt(uint(1), 2)) 19 | // MaxUInt("a", "b") // string does not implement UInt 20 | } 21 | -------------------------------------------------------------------------------- /lectures/07/code/palindrome/palindrome.go: -------------------------------------------------------------------------------- 1 | package palindrome 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | func IsPalindrome(s string) bool { 8 | var letters []rune 9 | for _, r := range s { 10 | if !unicode.IsLetter(r) { 11 | panic("123") 12 | } 13 | letters = append(letters, unicode.ToLower(r)) 14 | } 15 | 16 | for i := range letters { 17 | if letters[i] != letters[len(letters)-1-i] { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dbeliakov/mipt-golang-course 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.11 7 | github.com/stretchr/testify v1.8.4 8 | go.uber.org/goleak v1.3.0 9 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/02/multiple_pages.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, 2 | consectetur adipiscing elit, 3 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 4 | Ut enim ad minim veniam, 5 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 6 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 7 | Excepteur sint occaecat cupidatat non proident, 8 | sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /tasks/03/courses/applications.go: -------------------------------------------------------------------------------- 1 | package courses 2 | 3 | type StudentName struct { 4 | FirstName string 5 | LastName string 6 | } 7 | 8 | type CourseName string 9 | 10 | type StudentApplication struct { 11 | Name StudentName 12 | Avg uint32 13 | Priorities []CourseName 14 | } 15 | 16 | type Course struct { 17 | Name CourseName 18 | Limit uint32 19 | } 20 | 21 | func ProcessApplications(students []StudentApplication, courses []Course) map[CourseName][]StudentName { 22 | panic("implement me") 23 | } 24 | -------------------------------------------------------------------------------- /utils/testutils/tempfile.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TempFile(t *testing.T, content string) (fileName string) { 11 | t.Helper() 12 | 13 | f, err := os.CreateTemp(t.TempDir(), "test-*") 14 | require.NoError(t, err) 15 | _, err = f.WriteString(content) 16 | require.NoError(t, err) 17 | 18 | require.NoError(t, f.Close()) 19 | t.Cleanup(func() { 20 | _ = os.Remove(f.Name()) 21 | }) 22 | 23 | return f.Name() 24 | } 25 | -------------------------------------------------------------------------------- /lectures/01/code/fetch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | for _, url := range os.Args[1:] { 12 | resp, err := http.Get(url) 13 | if err != nil { 14 | fmt.Fprintf(os.Stderr, "fetch: %v\n", err) 15 | os.Exit(1) 16 | } 17 | b, err := ioutil.ReadAll(resp.Body) 18 | resp.Body.Close() 19 | if err != nil { 20 | fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) 21 | os.Exit(1) 22 | } 23 | fmt.Printf("%s", b) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tasks/04/rw/readers.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | type CountReader struct { 9 | } 10 | 11 | func NewCountReader() *CountReader { 12 | return &CountReader{} 13 | } 14 | 15 | var ErrLimitExceeded = errors.New("limit exceeded") 16 | 17 | type LimitReader struct { 18 | } 19 | 20 | func NewLimitReader(r io.Reader, limit int) *LimitReader { 21 | return &LimitReader{} 22 | } 23 | 24 | type ConcatReader struct { 25 | } 26 | 27 | func NewConcatReader(rs ...io.Reader) *ConcatReader { 28 | return &ConcatReader{} 29 | } 30 | -------------------------------------------------------------------------------- /lectures/09/code/inference/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | // Scale returns a copy of s with each element multiplied by c. 10 | func Scale[S ~[]E, E constraints.Integer](s S, c E) S { 11 | r := make(S, len(s)) 12 | for i, v := range s { 13 | r[i] = v * c 14 | } 15 | return r 16 | } 17 | 18 | type Point []int32 19 | 20 | func (p Point) String() string { 21 | return fmt.Sprintf("Point{%v}", []int32(p)) 22 | } 23 | 24 | func main() { 25 | fmt.Println(Scale(Point{1, 2}, 2).String()) 26 | } 27 | -------------------------------------------------------------------------------- /tasks/01/sum/sum_test.go: -------------------------------------------------------------------------------- 1 | package sum 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestSum_Empty(t *testing.T) { 10 | require.Equal(t, 0, Sum(nil)) 11 | } 12 | 13 | func TestSum_One(t *testing.T) { 14 | const num = 42 15 | require.Equal(t, num, Sum([]int{num})) 16 | } 17 | 18 | func TestSum_Couple(t *testing.T) { 19 | arr := []int{1, 2, 3, 10} 20 | require.Equal(t, 16, Sum(arr)) 21 | } 22 | 23 | func TestSum_Negative(t *testing.T) { 24 | arr := []int{1, 2, 3, -10} 25 | require.Equal(t, -4, Sum(arr)) 26 | } 27 | -------------------------------------------------------------------------------- /lectures/01/code/dup3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func main() { 11 | counts := make(map[string]int) 12 | for _, filename := range os.Args[1:] { 13 | data, err := ioutil.ReadFile(filename) 14 | if err != nil { 15 | fmt.Fprintf(os.Stderr, "dup3: %v\n", err) 16 | continue 17 | } 18 | for _, line := range strings.Split(string(data), "\n") { 19 | counts[line]++ 20 | } 21 | } 22 | for line, n := range counts { 23 | if n > 1 { 24 | fmt.Printf("%d\t%s\n", n, line) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/multiple_files/expected.txt: -------------------------------------------------------------------------------- 1 | testdata/multiple_files/a.txt Page: 0 2 | 3 | a 4 | b 5 | c 6 | d 7 | e 8 | f 9 | 10 | 11 | 12 | testdata/multiple_files/b.txt Page: 0 13 | 14 | Hello! 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | testdata/multiple_files/c.txt Page: 0 24 | 25 | #include 26 | 27 | int main() { 28 | std::ios_base::sync_with_stdio(false); 29 | std::cin.tie(nullptr); 30 | 31 | 32 | 33 | 34 | testdata/multiple_files/c.txt Page: 1 35 | 36 | 37 | std::cout << "Hello, World!\n"; 38 | 39 | return 0; 40 | } 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lectures/06/code/cancel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | func foo(ctx context.Context, wg *sync.WaitGroup) { 11 | select { 12 | case <-time.After(time.Second): 13 | fmt.Println("Finished") 14 | case <-ctx.Done(): 15 | fmt.Println("Aborted", ctx.Err()) 16 | } 17 | wg.Done() 18 | } 19 | 20 | func main() { 21 | ctx := context.Background() 22 | ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) 23 | defer cancel() 24 | 25 | var wg sync.WaitGroup 26 | wg.Add(1) 27 | go foo(ctx, &wg) 28 | 29 | wg.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /tasks/06/genericutils/utils.go: -------------------------------------------------------------------------------- 1 | package genericutils 2 | 3 | func Filter[T any](s []T, pred func(T) bool) []T { 4 | panic("implement me") 5 | } 6 | 7 | func GroupBy[T any, K comparable](s []T, grouper func(T) K) map[K][]T { 8 | panic("implement me") 9 | } 10 | 11 | func MaxBy[T any](s []T, less func(a T, b T) bool) T { 12 | panic("implement me") 13 | } 14 | 15 | func Repeat[T any](val T, times int) []T { 16 | panic("implement me") 17 | } 18 | 19 | func JSONParse[T any](data []byte) (T, error) { 20 | panic("implement me") 21 | } 22 | 23 | func Dedup[T comparable](s []T) []T { 24 | panic("implement me") 25 | } 26 | -------------------------------------------------------------------------------- /tasks/06/mockhelper/mock_helper.go: -------------------------------------------------------------------------------- 1 | package mockhelper 2 | 3 | type TestingT interface { 4 | Errorf(format string, args ...any) 5 | FailNow() 6 | Cleanup(cb func()) 7 | } 8 | 9 | type Matcher interface { 10 | Match(any) bool 11 | String() string 12 | } 13 | 14 | type Call interface { 15 | Return(returns ...any) 16 | } 17 | 18 | type MockHelper interface { 19 | ExpectCall(method string, args ...any) Call 20 | Call(method string, args ...any) []any 21 | Finish() 22 | } 23 | 24 | func Any() Matcher { 25 | panic("implement me") 26 | } 27 | 28 | func NewMockHelper(t TestingT) MockHelper { 29 | panic("implement me") 30 | } 31 | -------------------------------------------------------------------------------- /tasks/03/pr/testdata/02/multiple_pages.txt.output: -------------------------------------------------------------------------------- 1 | testdata/02/multiple_pages.txt Page: 0 2 | 3 | Lorem ipsum dolor sit amet, 4 | consectetur adipiscing elit, 5 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 6 | Ut enim ad minim veniam, 7 | 8 | 9 | 10 | testdata/02/multiple_pages.txt Page: 1 11 | 12 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 13 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 14 | Excepteur sint occaecat cupidatat non proident, 15 | sunt in culpa qui officia deserunt mollit anim id est laborum. 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tasks/02/caesar/README.md: -------------------------------------------------------------------------------- 1 | # Шифр Цезаря 2 | 3 | Напишите функцию `Cipher`, реализующую Шифр Цезаря для шифрования строки. На вход принимается строка и число-сдвиг. Программа должна "сдвинуть" каждую букву в строке на заданное число позиций в алфавите, циклически возвращаясь к началу алфавита, если необходимо. 4 | 5 | Буквы латинского алфавита (a-z и A-Z) должны быть сдвинуты, все остальные символы - остаться как есть. 6 | 7 | Вместо типа char в Go используется тип `rune`. Не вдаваясь пока в подробности, над `rune` можно производить операции с другими `rune`: 8 | ```go 9 | var r rune 10 | r += 'a' // сложение с рунами 11 | r += rune(1) // сложение с числами 12 | ``` 13 | -------------------------------------------------------------------------------- /tasks/05/dining/README.md: -------------------------------------------------------------------------------- 1 | # Стол с вилками для философов 2 | 3 | Для решения [классической задачи о философах](https://en.wikipedia.org/wiki/Dining_philosophers_problem) для начала нужно иметь стол с вилками, которыми философы могли бы есть. 4 | 5 | Принцип работы вилки можно описать так: если какой-то философ взял вилку методом `Acquire()`, то пока он не отпустит ее методом `Release()`, любой философ, пытающийся взять ту же самую вилку будет блокироваться на методе `Acquire()`. 6 | 7 | Важно, что философы не должны активно ожидать, пока вилку не отпустят, то есть вызовы Acquire должны быть блокирующими. 8 | 9 | Реализуйте тип Fork, который следует заданным требованиям, не используя пакет `"sync"`. -------------------------------------------------------------------------------- /tasks/05/downloader/downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type DownloadError struct { 9 | err error 10 | url string 11 | } 12 | 13 | func (d *DownloadError) URL() string { 14 | return d.url 15 | } 16 | 17 | func (d *DownloadError) Error() string { 18 | return fmt.Sprintf("download %q: %s", d.url, d.err.Error()) 19 | } 20 | 21 | func (d *DownloadError) Unwrap() error { 22 | return d.err 23 | } 24 | 25 | type Downloader struct { 26 | client *http.Client 27 | } 28 | 29 | func NewDownloader(client *http.Client) *Downloader { 30 | return &Downloader{ 31 | client: client, 32 | } 33 | } 34 | 35 | func (d *Downloader) Download(urls ...string) (map[string][]byte, []error) { 36 | panic("implement me") 37 | } 38 | -------------------------------------------------------------------------------- /tasks/05/datatransfer/transfer.go: -------------------------------------------------------------------------------- 1 | package datatransfer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ErrProducerFinished = errors.New("producer finished") 9 | 10 | type ProducerStorage interface { 11 | ReadNext() (any, error) 12 | } 13 | type ConsumerStorage interface { 14 | WriteMany([]any) error 15 | } 16 | 17 | type Transfer struct { 18 | writersCount int 19 | chunkSize int 20 | // your code here 21 | } 22 | 23 | func NewTransfer(writersCount, chunkSize int) *Transfer { 24 | return &Transfer{ 25 | writersCount: writersCount, 26 | chunkSize: chunkSize, 27 | // your code here 28 | } 29 | } 30 | 31 | func (t *Transfer) TransferData(ctx context.Context, p ProducerStorage, c ConsumerStorage) error { 32 | panic("implement me") 33 | } 34 | -------------------------------------------------------------------------------- /lectures/04/code/interfaces/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Foo struct{} 6 | 7 | func (f Foo) String() string { 8 | return "" 9 | } 10 | 11 | var _ fmt.Stringer = Foo{} 12 | var _ fmt.Stringer = &Foo{} 13 | 14 | type Bar struct{} 15 | 16 | func (b *Bar) String() string { 17 | return "" 18 | } 19 | 20 | // var _ fmt.Stringer = Bar{} // error: Type does not implement 'fmt.Stringer' as 'String' method has a pointer receiver 21 | var _ fmt.Stringer = &Bar{} 22 | 23 | // Nil interfaces 24 | 25 | type Err struct{} 26 | 27 | func (Err) Error() string { 28 | return "" 29 | } 30 | 31 | func foo() *Err { 32 | return nil 33 | } 34 | 35 | func bar() error { 36 | return foo() 37 | } 38 | 39 | func main() { 40 | err := bar() 41 | fmt.Printf("%v == nil? %v", err, err == nil) 42 | } 43 | -------------------------------------------------------------------------------- /lectures/01/code/dup2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | counts := make(map[string]int) 11 | files := os.Args[1:] 12 | if len(files) == 0 { 13 | countLines(os.Stdin, counts) 14 | } else { 15 | for _, arg := range files { 16 | f, err := os.Open(arg) 17 | if err != nil { 18 | fmt.Fprintf(os.Stderr, "dup2: %v\n", err) 19 | continue 20 | } 21 | countLines(f, counts) 22 | f.Close() 23 | } 24 | } 25 | for line, n := range counts { 26 | if n > 1 { 27 | fmt.Printf("%d\t%s\n", n, line) 28 | } 29 | } 30 | } 31 | 32 | func countLines(f *os.File, counts map[string]int) { 33 | input := bufio.NewScanner(f) 34 | for input.Scan() { 35 | counts[input.Text()]++ 36 | } 37 | // NOTE: ignoring potential errors from input.Err() 38 | } 39 | -------------------------------------------------------------------------------- /lectures/08/code/valueset/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func main() { 9 | var x = 2 10 | v := reflect.ValueOf(x) 11 | fmt.Println(v.CanAddr()) 12 | // v.Set(reflect.ValueOf(3)) // panic: reflect: reflect.Value.Set using unaddressable value 13 | fmt.Println("===") 14 | 15 | vp := reflect.ValueOf(&x).Elem() 16 | fmt.Println(vp.CanAddr()) 17 | px := vp.Addr().Interface().(*int) 18 | *px = 3 19 | fmt.Println(x) 20 | fmt.Println("===") 21 | 22 | vp.Set(reflect.ValueOf(4)) 23 | fmt.Println(x) 24 | fmt.Println("===") 25 | 26 | vp.SetInt(5) 27 | fmt.Println(x) 28 | fmt.Println("===") 29 | 30 | var i interface{} 31 | v = reflect.ValueOf(&i).Elem() 32 | fmt.Println(v.CanAddr()) 33 | // v.SetInt(1) // panic: reflect: call of reflect.Value.SetInt on interface Value 34 | v.Set(reflect.ValueOf(1)) 35 | fmt.Printf("%T: %v\n", i, i) 36 | } 37 | -------------------------------------------------------------------------------- /tasks/02/caesar/caesar_test.go: -------------------------------------------------------------------------------- 1 | package caesar_test 2 | 3 | import ( 4 | "github.com/dbeliakov/mipt-golang-course/tasks/02/caesar" 5 | "testing" 6 | ) 7 | 8 | func TestCipher(t *testing.T) { 9 | cases := []struct { 10 | name string 11 | input string 12 | shift int 13 | expect string 14 | }{ 15 | {"BasicLowerCase", "abc", 3, "def"}, 16 | {"BasicUpperCase", "ABC", 3, "DEF"}, 17 | {"WrapAround", "xyz", 3, "abc"}, 18 | {"FullSentence", "Hello, World!", 3, "Khoor, Zruog!"}, 19 | {"NegativeShift", "def", -3, "abc"}, 20 | {"ZeroShift", "abc", 0, "abc"}, 21 | {"NonAlphabeticCharacters", "123", 3, "123"}, 22 | } 23 | 24 | for _, c := range cases { 25 | t.Run(c.name, func(t *testing.T) { 26 | got := caesar.Cipher(c.input, c.shift) 27 | if got != c.expect { 28 | t.Errorf("caesarCipher(%q, %d) == %q, want %q", c.input, c.shift, got, c.expect) 29 | } 30 | }) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lectures/09/code/type_parameters/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | func min[T constraints.Ordered](a, b T) T { 10 | if a < b { 11 | return a 12 | } 13 | return b 14 | } 15 | 16 | type Tree[T interface{}] struct { 17 | left, right *Tree[T] 18 | value T 19 | } 20 | 21 | func (t *Tree[T]) Lookup(x T) *Tree[T] { 22 | return nil 23 | } 24 | 25 | func main() { 26 | fmt.Println(min[int](1, 2), min[float32](2.0, 1.0), min[string]("foo", "bar")) 27 | fmt.Println(min(1, 2), min(2.0, 1.0), min("foo", "bar")) 28 | // min([]string{}, []string{}) // []string does not implement constraints.Ordered 29 | // min(1, 1.0) // default type float64 of 1.0 does not match inferred type int for T 30 | 31 | var stringTree Tree[string] 32 | stringTree.Lookup("foo") 33 | // stringTree.Lookup(1) // Cannot use '1' (type untyped int) as the type T (string) 34 | } 35 | -------------------------------------------------------------------------------- /tasks/03/polish/README.md: -------------------------------------------------------------------------------- 1 | # Вычисление обратной польской нотации 2 | 3 | Реализуйте функцию `Calculate(expr string) (int, error)`, 4 | которая вычисляет значение выражения, записанного в [обратной польской нотации](https://en.wikipedia.org/wiki/Reverse_Polish_notation). 5 | 6 | На вход функция получает строку, состоящую из цифр и операторов `+`, `-`, `*`. 7 | Гарантируется, что все элементы выражения будут разделены одним пробелом. 8 | 9 | Помимо значения выражения функция возвращает ошибку в случае, если само выражение оказалось невалидно. 10 | В таком случае вам нужно вернуть уже существующую ошибку `ErrInvalidExpression`. 11 | Примеры невалидных выражений вы можете найти в тестах. 12 | 13 | Пример: 14 | ```go 15 | res, err := Calculate("3 5 +") 16 | fmt.Println(res, err) // 8 nil 17 | ``` 18 | 19 | Вам могут пригодиться: 20 | - [strings.Split](https://pkg.go.dev/strings#Split) 21 | - [strconv.Atoi](https://pkg.go.dev/strconv#Atoi) 22 | -------------------------------------------------------------------------------- /lectures/05/code/clock/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | // See page 222. 5 | 6 | // Clock is a TCP server that periodically writes the time. 7 | package main 8 | 9 | import ( 10 | "io" 11 | "log" 12 | "net" 13 | "time" 14 | ) 15 | 16 | func handleConn(c net.Conn) { 17 | defer func() { 18 | _ = c.Close() 19 | }() 20 | for { 21 | _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) 22 | if err != nil { 23 | return // e.g., client disconnected 24 | } 25 | time.Sleep(1 * time.Second) 26 | } 27 | } 28 | 29 | func main() { 30 | listener, err := net.Listen("tcp", "localhost:8000") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | for { 35 | conn, err := listener.Accept() 36 | if err != nil { 37 | log.Print(err) // e.g., connection aborted 38 | continue 39 | } 40 | go handleConn(conn) // handle connections concurrently 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Программирование на Golang 2 | 3 | [Таблица с результатами](https://go.dbeliakov.ru) 4 | 5 | ### Домашние задания 6 | 1. [Введение](tasks/01) - **дедлайн 27 февраля** 7 | 2. [Базовые конструкции языка](tasks/02) - **дедлайн 13 марта** 8 | 3. [Составные типы. Ошибки](tasks/03) - **дедлайн 13 марта** 9 | 4. [Defer и Panic. Интерфейсы. Ошибки](tasks/04) - **дедлайн 3 апреля** 10 | 5. [Конкурентность и параллелизм. Горутины. Каналы. Механизмы синхронизации](tasks/05) - **дедлайн 17 апреля** 11 | 6. [Тестирование. Reflection. Generics](tasks/06) - **дедлайн 22 мая** 12 | 13 | ### Материалы курса 14 | 1. [Введение](lectures/01) 15 | 2. [Базовые конструкции языка](lectures/02) 16 | 3. [Составные типы. Ошибки](lectures/03) 17 | 4. [Defer и Panic. Интерфейсы. Ошибки](lectures/04) 18 | 5. [Конкурентность и параллелизм. Горутины. Каналы](lectures/05) 19 | 6. [Механизмы синхронизации](lectures/06) 20 | 7. [Тестирование](lectures/07) 21 | 8. [Reflection](lectures/08) 22 | 9. [Generics](lectures/09) 23 | -------------------------------------------------------------------------------- /lectures/01/code/fetchall/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | start := time.Now() 14 | ch := make(chan string) 15 | for _, url := range os.Args[1:] { 16 | go fetch(url, ch) // start a goroutine 17 | } 18 | for range os.Args[1:] { 19 | fmt.Println(<-ch) // receive from channel ch 20 | } 21 | fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds()) 22 | } 23 | 24 | func fetch(url string, ch chan<- string) { 25 | start := time.Now() 26 | resp, err := http.Get(url) 27 | if err != nil { 28 | ch <- fmt.Sprint(err) // send to channel ch 29 | return 30 | } 31 | 32 | nbytes, err := io.Copy(ioutil.Discard, resp.Body) 33 | resp.Body.Close() // don't leak resources 34 | if err != nil { 35 | ch <- fmt.Sprintf("while reading %s: %v", url, err) 36 | return 37 | } 38 | secs := time.Since(start).Seconds() 39 | ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) 40 | } 41 | -------------------------------------------------------------------------------- /tasks/04/retries/api.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrTimeout = errors.New("timeout") 9 | ErrNetworkFault = errors.New("network error") 10 | ) 11 | 12 | type Status string 13 | 14 | const ( 15 | StatusNotFound Status = "not found" 16 | StatusValueTooOld Status = "value too old" 17 | StatusFatalError Status = "fatal error" 18 | ) 19 | 20 | type APIError struct { 21 | status Status 22 | } 23 | 24 | func (a APIError) Error() string { 25 | return "encountered api error: " + string(a.status) 26 | } 27 | 28 | func (a APIError) Status() Status { 29 | return a.status 30 | } 31 | 32 | type Value struct{} 33 | 34 | type SimpleAPI interface { 35 | Get(key string) (val *Value, epoch uint64, err error) 36 | Set(key string, targetEpoch uint64, value *Value) error 37 | } 38 | 39 | type ValueUpdater func(currentValue *Value) (*Value, error) 40 | 41 | func UpdateValue(api SimpleAPI, key string, update ValueUpdater) error { 42 | panic("implement me") 43 | } 44 | -------------------------------------------------------------------------------- /tasks/06/formvalues/README.md: -------------------------------------------------------------------------------- 1 | # Form Values Unpacker 2 | 3 | Вам необходимо реализовать декодированиe данных HTML-форм (form values) в структуры Go. 4 | 5 | Реализуйте функцию: 6 | ```go 7 | func Unpack(req *http.Request, ptr interface{}) error 8 | ``` 9 | 10 | Которая должна: 11 | - Парсить form values из HTTP-запроса 12 | - Заполнять переданную структуру (ptr) соответствующими значениями 13 | - Поддерживать базовые типы: string, int, bool, []string 14 | - Использовать тег для указания имен полей формы 15 | - Возвращать ошибки при: 16 | - Несоответствии типов 17 | - Передаче не-указателя 18 | - Неподдерживаемых типах полей 19 | 20 | Вам могут пригодиться: 21 | - [http.Request](https://pkg.go.dev/net/http#Request) 22 | - [http.Request.ParseForm](https://pkg.go.dev/net/http#Request.ParseForm) 23 | - [reflect.ValueOf](https://pkg.go.dev/reflect#ValueOf) 24 | - [reflect.Value](https://pkg.go.dev/reflect#Value) 25 | - [reflect.Type](https://pkg.go.dev/reflect#Type) 26 | - [strconv](https://pkg.go.dev/strconv) 27 | -------------------------------------------------------------------------------- /tasks/02/lcp/README.md: -------------------------------------------------------------------------------- 1 | # Наибольший общий префикс 2 | 3 | Реализуйте функцию `LongestCommonPrefix`, которая находит наибольший общий префикс двух строк. Имейте в виду, что строки в этой задаче не имеют ограничений в используемых символах, а значит представляют собой `UTF-8`-последовательности. Ваша функция должна корректно обрабатывать `UTF-8`-руны, не обрезая их. С этим вам может помочь пакет [`unicode/utf8`](https://pkg.go.dev/unicode/utf8), изучите его возможности. 4 | 5 | В этой задаче есть бенчмарки, проверяющие эффективность вашей реализации, результат бенчмарка авторского решения: 6 | ``` 7 | goos: darwin 8 | goarch: arm64 9 | pkg: github.com/dbeliakov/mipt-golang-course/tasks/02/lcp 10 | BenchmarkLongestCommonPrefix 11 | BenchmarkLongestCommonPrefix-12 2320185 520.5 ns/op 248 B/op 5 allocs/op 12 | PASS 13 | ``` 14 | Принцип работы с бенчмарками и процесс аналиа профилей описан в задаче [`rle`](../rle/README.md#запуск-бенчмарков-и-анализ-профилей). Рекомендуется сначала решить именно `rle`, и после этого переходить к данной задаче. 15 | -------------------------------------------------------------------------------- /tasks/06/genericutils/README.md: -------------------------------------------------------------------------------- 1 | # Generic Utilities 2 | 3 | Реализуйте набор универсальных функций для работы с обобщенными типами (generics), которые часто требуются в проектах. 4 | 5 | ## Требуемые функции 6 | 7 | ```go 8 | // Filter - фильтрация элементов слайса по условию 9 | func Filter[T any](s []T, pred func(T) bool) []T 10 | 11 | // GroupBy - группировка элементов по ключу 12 | func GroupBy[T any, K comparable](s []T, grouper func(T) K) map[K][]T 13 | 14 | // MaxBy - поиск максимального элемента по критерию 15 | func MaxBy[T any](s []T, less func(a, b T) bool) T 16 | 17 | // Repeat - создание слайса с повторяющимся значением 18 | func Repeat[T any](val T, times int) []T 19 | 20 | // JSONParse - парсинг JSON в указанный тип 21 | func JSONParse[T any](data []byte) (T, error) 22 | 23 | // Dedup - удаление дубликатов из слайса 24 | func Dedup[T comparable](s []T) []T 25 | ``` 26 | 27 | Вам могут пригодиться: 28 | - [Go Generics Tutorial](https://go.dev/doc/tutorial/generics) 29 | - [Type Parameters Proposal](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md) 30 | -------------------------------------------------------------------------------- /tasks/05/philosophers/README.md: -------------------------------------------------------------------------------- 1 | # Обедающие философы 2 | 3 | За круглым столом сидят $n$ философов. Перед каждым из них стоит тарелка с пастой. Между каждой парой соседних философов лежит вилка. 4 | 5 | Каждый философ за столом следует алгоритму: 6 | 7 | 1) Берет две лежащие рядом вилки 8 | 2) Ест пасту 9 | 3) Кладет вилки обратно на стол 10 | 4) Размышляет над чем-то 11 | 5) Возвращается к шагу 1 12 | 13 | Зачем есть пасту двумя вилками – не ясно, но так устроены философы. 14 | 15 | Разумеется, два сидящих рядом философа не могут есть одновременно, поскольку они делят между собой по крайней мере одну вилку. 16 | 17 | Вам нужно научить философов брать и отпускать вилки таким образом, чтобы они не могли навечно заблокировать друг друга и умереть от голода. 18 | 19 | ## Вилки 20 | 21 | Для выполнения этой задачи, для начала вам нужно выполнить задание [dining](../dining/). 22 | 23 | Как вы могли понять по вышеуказанной задаче, с вилками можно делать только два действия: взять и положить. 24 | Нельзя проверять, взяли ли уже вилку или нет. 25 | 26 | Также вам известен номер сидения и номер вилок, которые вы будете брать. -------------------------------------------------------------------------------- /tasks/05/philosophers/problem_test.go: -------------------------------------------------------------------------------- 1 | package philosophers 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/dbeliakov/mipt-golang-course/tasks/05/dining" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const seatCount = 10 14 | 15 | func TestDining(t *testing.T) { 16 | table := dining.NewTable(seatCount) 17 | 18 | philosophers := make([]*Philosopher, 0, seatCount) 19 | for i := 0; i < seatCount; i++ { 20 | philosophers = append(philosophers, NewPhilosopher(table, i)) 21 | } 22 | 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) 24 | defer cancel() 25 | var wg sync.WaitGroup 26 | 27 | for i := 0; i < seatCount; i++ { 28 | wg.Add(1) 29 | 30 | go func(i int) { 31 | defer wg.Done() 32 | for c := 0; c < 100; c++ { 33 | select { 34 | case <-ctx.Done(): 35 | return 36 | default: 37 | philosophers[i].Dine() 38 | philosophers[i].Think() 39 | } 40 | } 41 | }(i) 42 | } 43 | 44 | wg.Wait() 45 | for _, p := range philosophers { 46 | assert.Greater(t, p.EatsCount(), 0) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tasks/02/reverse/README.md: -------------------------------------------------------------------------------- 1 | # Reverse String 2 | 3 | Реализуйте функцию `ReverseString`, которая возвращет строку, состоящую из `UTF-8`-рун входной строки, записанных в обратном порядке. В этой задаче, как и в [`lcp`](../lcp/README.md), важно сохранить корректность рун, входящих в строку. Предлагается сначала решить упомянутую задачу, после чего переходить к этой. 4 | 5 | > В тестах вы можете обратить внимание на то, что некоторые графемы при развороте распадаются. Это происходит из-за того, что в юникоде несколько рун могут сливаться в одну графему, например эмодзи "🇩🇪" состоит из двух рун "🇩" и "🇪" 6 | 7 | При развороте строки важно не делать лишних аллокаций, чтобы не тратить память и время. Конкретно, ожидается, что ваш код будет делать не более одной-двух аллокаций. Посмотреть количество аллокаций можно на бенчмарке. 8 | 9 | Результаты бенчмарка авторского решения: 10 | ``` 11 | goos: darwin 12 | goarch: arm64 13 | pkg: github.com/dbeliakov/mipt-golang-course/tasks/02/reverse 14 | BenchmarkReverseString 15 | BenchmarkReverseString-12 106069 11391 ns/op 4096 B/op 1 allocs/op 16 | PASS 17 | ``` 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dmitrii Beliakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tasks/05/philosophers/problem.go: -------------------------------------------------------------------------------- 1 | package philosophers 2 | 3 | import ( 4 | "math/rand/v2" 5 | "time" 6 | 7 | "github.com/dbeliakov/mipt-golang-course/tasks/05/dining" 8 | ) 9 | 10 | type Philosopher struct { 11 | seat int 12 | table *dining.Table 13 | l, r *dining.Fork 14 | eatsCount int 15 | } 16 | 17 | func NewPhilosopher(table *dining.Table, seat int) *Philosopher { 18 | l := table.LeftFork(seat) 19 | r := table.RightFork(seat) 20 | 21 | return &Philosopher{ 22 | seat: seat, 23 | table: table, 24 | l: l, 25 | r: r, 26 | } 27 | } 28 | 29 | func (p *Philosopher) EatsCount() int { 30 | return p.eatsCount 31 | } 32 | 33 | func (p *Philosopher) Dine() { 34 | p.AcquireForks() 35 | defer p.ReleaseForks() 36 | p.Eat() 37 | } 38 | 39 | func (p *Philosopher) Think() { 40 | sleepTime := 500 + rand.Int()%500 41 | time.Sleep(time.Duration(sleepTime) * time.Millisecond) 42 | } 43 | 44 | func (p *Philosopher) Eat() { 45 | p.eatsCount++ 46 | } 47 | 48 | func (p *Philosopher) AcquireForks() { 49 | panic("implement me") 50 | } 51 | 52 | func (p *Philosopher) ReleaseForks() { 53 | panic("implement me") 54 | } 55 | -------------------------------------------------------------------------------- /tasks/02/rle/rle_test.go: -------------------------------------------------------------------------------- 1 | package rle 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRLECompress(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | input string 14 | expected string 15 | }{ 16 | { 17 | name: "empty", 18 | input: "", 19 | expected: "", 20 | }, 21 | { 22 | name: "one repeating symbol", 23 | input: "aaaaaaaaaaaaa", 24 | expected: "a13", 25 | }, 26 | { 27 | name: "non-repeating symbols", 28 | input: "some string", 29 | expected: "s1o1m1e1 1s1t1r1i1n1g1", 30 | }, 31 | { 32 | name: "mixed", 33 | input: "TTTATTAAAAC", 34 | expected: "T3A1T2A4C1", 35 | }, 36 | } 37 | 38 | for _, tc := range testCases { 39 | t.Run(tc.name, func(t *testing.T) { 40 | actual := RLECompress(tc.input) 41 | assert.Equal(t, tc.expected, actual) 42 | }) 43 | } 44 | } 45 | 46 | func BenchmarkRLECompress(b *testing.B) { 47 | input := strings.Repeat("GCTAGTTATTGGGG", 100) 48 | 49 | b.ReportAllocs() 50 | 51 | for i := 0; i < b.N; i++ { 52 | _ = RLECompress(input) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tasks/05/dining/fork.go: -------------------------------------------------------------------------------- 1 | package dining 2 | 3 | type Fork struct { 4 | // your code here 5 | } 6 | 7 | func NewFork() *Fork { 8 | return &Fork{ 9 | // your code here 10 | } 11 | } 12 | 13 | func (f *Fork) Acquire() { 14 | panic("implement me") 15 | } 16 | 17 | func (f *Fork) Release() { 18 | panic("implement me") 19 | } 20 | 21 | type Table struct { 22 | seats int 23 | forks []*Fork 24 | } 25 | 26 | func NewTable(seats int) *Table { 27 | forks := make([]*Fork, 0, seats) 28 | for i := 0; i < seats; i++ { 29 | forks = append(forks, NewFork()) 30 | } 31 | 32 | return &Table{ 33 | seats: seats, 34 | forks: forks, 35 | } 36 | } 37 | 38 | func (t *Table) SeatsCount() int { 39 | return t.seats 40 | } 41 | 42 | func (t *Table) LeftForkIdx(seatNum int) int { 43 | return seatNum 44 | } 45 | 46 | func (t *Table) RightForkIdx(seatNum int) int { 47 | rightIdx := seatNum - 1 48 | if rightIdx < 0 { 49 | rightIdx = t.seats - 1 50 | } 51 | 52 | return rightIdx 53 | } 54 | 55 | func (t *Table) LeftFork(seatNum int) *Fork { 56 | return t.forks[t.LeftForkIdx(seatNum)] 57 | } 58 | 59 | func (t *Table) RightFork(seatNum int) *Fork { 60 | return t.forks[t.RightForkIdx(seatNum)] 61 | } 62 | -------------------------------------------------------------------------------- /tasks/02/lcp/lcp_test.go: -------------------------------------------------------------------------------- 1 | package lcp 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLongestCommonPrefix(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | l string 14 | r string 15 | expected string 16 | }{ 17 | {name: "two empty strings", l: "", r: "", expected: ""}, 18 | {name: "one empty string", l: "", r: "Hello!", expected: ""}, 19 | {name: "basic", l: "Hello World!", r: "Hello, World!", expected: "Hello"}, 20 | {name: "equal strings", l: "abacaba", r: "abacaba", expected: "abacaba"}, 21 | {name: "one is a prefix of another", l: "Hello, World!", r: "Hello", expected: "Hello"}, 22 | {name: "no common prefix", l: "Hello", r: "hello", expected: ""}, 23 | {name: "emoji", l: "😀", r: "😁", expected: ""}, 24 | } 25 | 26 | for _, tc := range testCases { 27 | t.Run(tc.name, func(t *testing.T) { 28 | actual := LongestCommonPrefix(tc.l, tc.r) 29 | assert.Equal(t, tc.expected, actual) 30 | }) 31 | } 32 | } 33 | 34 | func BenchmarkLongestCommonPrefix(b *testing.B) { 35 | l, r := strings.Repeat("a", 100)+"😀", strings.Repeat("a", 100)+"😁" 36 | 37 | b.ReportAllocs() 38 | for i := 0; i < b.N; i++ { 39 | _ = LongestCommonPrefix(l, r) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tasks/02/reverse/reverse_string_test.go: -------------------------------------------------------------------------------- 1 | package reverse 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestReverseString(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | input string 14 | expected string 15 | }{ 16 | {name: "empty", input: "", expected: ""}, 17 | {name: "one symbol", input: "a", expected: "a"}, 18 | {name: "basic string", input: "Hello, World!", expected: "!dlroW ,olleH"}, 19 | {name: "russian string", input: "Строка на русском", expected: "мокссур ан акортС"}, 20 | {name: "sequences", input: "\r\n\t\b", expected: "\b\t\n\r"}, 21 | {name: "simple hieroglyphs", input: "Hello, 世界", expected: "界世 ,olleH"}, 22 | {name: "complex hieroglyph", input: "뢴", expected: "ᆫᅬᄅ"}, 23 | {name: "emoji", input: "🙂🙂", expected: "🙂🙂"}, 24 | } 25 | 26 | for _, tc := range testCases { 27 | t.Run(tc.name, func(t *testing.T) { 28 | actual := ReverseString(tc.input) 29 | assert.Equal(t, tc.expected, actual) 30 | }) 31 | } 32 | } 33 | 34 | func BenchmarkReverseString(b *testing.B) { 35 | input := strings.Repeat("世界 🙂🙂 \rПривет, World!", 100) 36 | 37 | b.ReportAllocs() 38 | 39 | for i := 0; i < b.N; i++ { 40 | _ = ReverseString(input) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lectures/08/code/value/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | fmt.Println(Any(3), Any(true), Any(time.Now()), Any("hello"), Any(nil), Any(make(chan int))) 12 | } 13 | 14 | func Any(v interface{}) string { 15 | return formatAtom(reflect.ValueOf(v)) 16 | } 17 | 18 | // formatAtom formats a value without inspecting its internal structure. 19 | func formatAtom(v reflect.Value) string { 20 | switch v.Kind() { 21 | case reflect.Invalid: 22 | return "invalid" 23 | case reflect.Int, reflect.Int8, reflect.Int16, 24 | reflect.Int32, reflect.Int64: 25 | return strconv.FormatInt(v.Int(), 10) 26 | case reflect.Uint, reflect.Uint8, reflect.Uint16, 27 | reflect.Uint32, reflect.Uint64, reflect.Uintptr: 28 | return strconv.FormatUint(v.Uint(), 10) 29 | // ...floating-point and complex cases omitted for brevity... 30 | case reflect.Bool: 31 | return strconv.FormatBool(v.Bool()) 32 | case reflect.String: 33 | return strconv.Quote(v.String()) 34 | case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: 35 | return v.Type().String() + " 0x" + 36 | strconv.FormatUint(uint64(v.Pointer()), 16) 37 | default: // reflect.Array, reflect.Struct, reflect.Interface 38 | return "<" + v.Type().String() + " value>" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tasks/04/retries/README.md: -------------------------------------------------------------------------------- 1 | # API Retries 2 | 3 | При работе с каким-нибудь API достаточно часто возникают ошибки, которые зачастую надо еще и ретраить. 4 | 5 | Допустим, у нас есть простой интерфейс key-value кэша: 6 | ```go 7 | type SimpleAPI interface { 8 | Get(key string) (val *Value, epoch uint64, err error) 9 | Set(key string, targetEpoch uint64, value *Value) error 10 | } 11 | ``` 12 | 13 | Вам нужно написать функцию, которая принимает реализацию описанного интерфейса, ключ и функцию, которая обновляет значение. Функция должна по ключу получить значение из API, обновить его, используя полученную функцию, и сохранить. В процессе могут возникать разные виды ошибок: 14 | - таймауты и ошибки сети (`ErrTimeout`, `ErrNetworkFault`) нужно ретраить 15 | - если значение по ключу не нашлось (`APIError.Status() == StatusNotFound`) или произошла фатальная ошибка на стороне апи (`APIError.Status() == StatusFatalError`), ошибку нужно вернуть из функции 16 | - если кто-то уже успел обновить значение быстрее нас (`APIError.Status() == StatusValueTooOld`), нужно снова попробовать получить значение по ключу и обновлять уже новое значение 17 | 18 | Для данной задачи допустим, что ретраить можно без ожидания между попытками бесконечное количество раз. 19 | 20 | Вам пригодятся: 21 | - [errors.Is](https://pkg.go.dev/errors#Is) 22 | - [errors.As](https://pkg.go.dev/errors#As) 23 | -------------------------------------------------------------------------------- /tasks/03/polish/calculate_test.go: -------------------------------------------------------------------------------- 1 | package polish 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type WantResult int 11 | 12 | const ( 13 | Value WantResult = 0 14 | AnyErr WantResult = 1 15 | InvalidExpressionErr WantResult = 2 16 | ) 17 | 18 | func TestCalculate(t *testing.T) { 19 | testCases := []struct { 20 | expr string 21 | expectedValue int 22 | wantResult WantResult 23 | }{ 24 | {expr: "", expectedValue: 0}, 25 | {expr: "1 2 +", expectedValue: 3}, 26 | {expr: "12 15 -", expectedValue: -3}, 27 | {expr: "1 4 56 + +", expectedValue: 61}, 28 | {expr: "-3 4 12 -15 - * *", expectedValue: -324}, 29 | {expr: "a b +", wantResult: AnyErr}, 30 | {expr: "3 4 5 +", wantResult: InvalidExpressionErr}, 31 | {expr: "1 *", wantResult: InvalidExpressionErr}, 32 | } 33 | 34 | for _, tc := range testCases { 35 | t.Run(tc.expr, func(t *testing.T) { 36 | res, err := Calculate(tc.expr) 37 | 38 | switch tc.wantResult { 39 | case Value: 40 | require.NoError(t, err) 41 | assert.Equal(t, tc.expectedValue, res) 42 | case AnyErr: 43 | assert.Error(t, err) 44 | case InvalidExpressionErr: 45 | require.Error(t, err) 46 | assert.ErrorIs(t, err, ErrInvalidExpression) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utils/testutils/binary.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "sync" 12 | ) 13 | 14 | type BinaryCache struct { 15 | dir string 16 | cache sync.Map 17 | } 18 | 19 | func NewBinaryCache() *BinaryCache { 20 | dir, err := os.MkdirTemp(os.TempDir(), "binarycache-*") 21 | if err != nil { 22 | log.Fatalf("Failed to create temporary directory: %v", err) 23 | } 24 | 25 | return &BinaryCache{dir: dir} 26 | } 27 | 28 | func (c *BinaryCache) LoadBinary(importPath string) string { 29 | p, ok := c.cache.Load(importPath) 30 | if ok { 31 | return p.(string) 32 | } 33 | 34 | binPath := filepath.Join(c.dir, RandomBinaryName()) 35 | cmd := exec.Command("go", "build", "-o", binPath, importPath) 36 | cmd.Env = append(os.Environ(), "GOFLAGS=") 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | if err := cmd.Run(); err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | c.cache.Store(importPath, binPath) 44 | return binPath 45 | } 46 | 47 | func (c *BinaryCache) Clear() { 48 | _ = os.RemoveAll(c.dir) 49 | } 50 | 51 | func RandomBinaryName() string { 52 | name := RandomName() 53 | if runtime.GOOS == "windows" { 54 | name += ".exe" 55 | } 56 | return name 57 | } 58 | 59 | func RandomName() string { 60 | var raw [8]byte 61 | _, _ = rand.Read(raw[:]) 62 | name := hex.EncodeToString(raw[:]) 63 | return name 64 | } 65 | -------------------------------------------------------------------------------- /lectures/07/code/palindrome/palindrome_test.go: -------------------------------------------------------------------------------- 1 | package palindrome 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsPalindrome(t *testing.T) { 11 | var testCases = []struct { 12 | string string 13 | isPalindrome bool 14 | name string 15 | }{ 16 | { 17 | string: "detartrated", 18 | isPalindrome: true, 19 | }, 20 | { 21 | string: "kayak", 22 | isPalindrome: true, 23 | }, 24 | { 25 | string: "palindrome", 26 | isPalindrome: false, 27 | }, 28 | { 29 | string: "été", 30 | isPalindrome: true, 31 | name: "french", 32 | }, 33 | } 34 | for _, tc := range testCases { 35 | tc := tc 36 | t.Run(tc.name, func(t *testing.T) { 37 | t.Parallel() 38 | assert.Equal(t, tc.isPalindrome, IsPalindrome(tc.string)) 39 | }) 40 | } 41 | } 42 | 43 | // func TestRandomPalindromes(t *testing.T) { 44 | // seed := time.Now().UTC().UnixNano() 45 | // t.Logf("Random seed = %v", seed) 46 | // rnd := rand.New(rand.NewSource(seed)) 47 | // for i := 0; i < 1000; i++ { 48 | // p := randomPalindrome(rnd) 49 | // if !IsPalindrome(p) { 50 | // t.Fail() 51 | // } 52 | // } 53 | // } 54 | 55 | func randomPalindrome(rnd *rand.Rand) string { 56 | n := rnd.Intn(25) 57 | runes := make([]rune, n) 58 | for i := 0; i < (n+1)/2; i++ { 59 | r := rune(rnd.Intn(0x1000)) // '\u0999' 60 | runes[i] = r 61 | runes[n-1-i] = r 62 | } 63 | return string(runes) 64 | } 65 | -------------------------------------------------------------------------------- /lectures/06/code/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/go-chi/chi/v5" 11 | ) 12 | 13 | func main() { 14 | r := chi.NewRouter() 15 | 16 | r.Get("/info", func(rw http.ResponseWriter, req *http.Request) { 17 | uid, err := UserIDFromContext(req.Context()) 18 | if err != nil { 19 | http.Error(rw, err.Error(), http.StatusInternalServerError) 20 | return 21 | } 22 | fmt.Println("Req") 23 | rw.WriteHeader(http.StatusOK) 24 | _, _ = rw.Write([]byte(fmt.Sprintf("Hello, %d", uid))) 25 | }) 26 | 27 | if err := http.ListenAndServe("localhost:8080", r); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | 32 | func Auth(next http.Handler) http.Handler { 33 | fn := func(rw http.ResponseWriter, req *http.Request) { 34 | authValue := req.Header.Get("Authorization") 35 | // Parse token and auth 36 | userID, err := strconv.Atoi(authValue) 37 | if err != nil { 38 | http.Error(rw, err.Error(), http.StatusBadRequest) 39 | return 40 | } 41 | 42 | next.ServeHTTP(rw, req.WithContext(ContextWithUserID(req.Context(), userID))) 43 | } 44 | return http.HandlerFunc(fn) 45 | } 46 | 47 | type userIDKey struct{} 48 | 49 | func ContextWithUserID(ctx context.Context, userID int) context.Context { 50 | return context.WithValue(ctx, userIDKey{}, userID) 51 | } 52 | 53 | func UserIDFromContext(ctx context.Context) (int, error) { 54 | uid := ctx.Value(userIDKey{}) 55 | if uid == nil { 56 | return 0, fmt.Errorf("no user id in context") 57 | } 58 | return uid.(int), nil 59 | } 60 | -------------------------------------------------------------------------------- /tasks/05/datatransfer/README.md: -------------------------------------------------------------------------------- 1 | # Datatransfer 2 | 3 | Вам нужно импортировать данные из одного хранилища в другое, при этом вы знаете, что: 4 | - хранилище-источник позволяет работать одновременно только с одним читателем 5 | - хранилище-приемник позволяет использовать сразу несколько писателей и записывает элементы почанково 6 | 7 | Необходимо написать код, который будет асинхронно читать записи из одного хранилища и писать в другое. 8 | Если в ходе чтения произошла ошибка `ErrProducerFinished`, считайте, что все записи были прочтены. 9 | Если в ходе чтения или записи произошла любая другая ошибка, то трансфер надо завершить с этой ошибкой. 10 | 11 | Также вы должны корректно обрабатывать отмену пришедшего вам на вход контекста: 12 | кто-то из пользователей мог выставить на трансфер таймаут, и код должен завершиться с ошибкой `ctx.Err()`, если трансфер не был завершен по истечении таймаута контекста. 13 | 14 | Проверка на истечение контекста происходит с помощью чтения из канала `ctx.Done()`. 15 | Самое частое использование -- ожидать одновременно двух или более болкирующих событий, одним из которых является отмена контекста: 16 | 17 | ```go 18 | select { 19 | case <-ctx.Done(): 20 | return ctx.Err() 21 | case data := <- dataChan: 22 | // do smth 23 | ... 24 | } 25 | ``` 26 | 27 | Перед выполнением задачи рекомендуется ознакомиться с контрактом, 28 | предоставляемым [context.Context](https://pkg.go.dev/context#Context) 29 | 30 | В этой задаче вам могут пригодиться: 31 | - [sync.WaitGroup](https://pkg.go.dev/sync#WaitGroup) 32 | - [errors.Join](https://pkg.go.dev/errors#Join) 33 | -------------------------------------------------------------------------------- /lectures/01/README.md: -------------------------------------------------------------------------------- 1 | # Лекция 1. Введение 2 | 3 | * [Слайды](https://dbeliakov.github.io/mipt-golang-course/lectures/01/lecture.slide.html) 4 | 5 | * [Официальный сайт](https://golang.org/) 6 | * [The Go Playground](https://play.golang.org/) 7 | * [The Go Programming Language](https://www.gopl.io/) (есть на русском языке) 8 | 9 | ### Сдача заданий 10 | 1. Сделать приватный fork репозитория 11 | 2. Дать пользователю [dbeliakov](https://github.com/dbeliakov) доступ до репозитория 12 | 3. Прислать в чат Имя, Фамилию и ссылку на fork репозитория 13 | 14 | ### Приватный fork публичного репозитория 15 | 16 | 1. Склонировать репозиторий (с флагом `bare`) 17 | ```shell 18 | git clone --bare git@github.com:dbeliakov/mipt-golang-course.git 19 | ``` 20 | 2. Создать на GitHub приватный репозиторий 21 | 3. Сделать mirror-push склонированного репозитория в только что созданный приватный 22 | ```shell 23 | cd mipt-golang-course.git 24 | git push --mirror git@github.com:/.git 25 | ``` 26 | 4. Удалить склонированный репозиторий 27 | ```shell 28 | cd .. 29 | rm -rf mipt-golang-course.git 30 | ``` 31 | 5. Склонировать приватный репозиторий 32 | ```shell 33 | git clone git@github.com:/.git 34 | ``` 35 | 6. Добавить remote на оригинальный репозиторий, чтобы можно было получать обновления 36 | ```shell 37 | git remote add upstream git@github.com:dbeliakov/mipt-golang-course.git 38 | git remote set-url --push upstream DISABLE 39 | ``` 40 | 7. Чтобы получить изменения из upstream, нужно выполнить 41 | ```shell 42 | git fetch upstream 43 | git rebase upstream/master 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /tasks/05/downloader/README.md: -------------------------------------------------------------------------------- 1 | # Асинхронный загрузчик 2 | 3 | В данной задаче вам нужно написать асинхронный загрузчик произвольных `URL`. 4 | 5 | Загрузчик принимает на вход произвольное количество произвольных `URL` и возвращает хэш-таблицу с контентом по ключу `URL` `map[string][]byte`. 6 | 7 | Помимо таблицы с контентом для страниц, скачать которые удалось успешно, загрузчик должен возвращать слайс ошибок `[]error` для каждой страницы, которую скачать не удалось. 8 | Все ошибки в слайсе должны иметь тип `DownloadError` и иметь заполненные поля `url` и `err`. 9 | 10 | ## http.Response 11 | 12 | Также в этой задаче вам предстоит поработать с [http.Response](https://pkg.go.dev/net/http#Response) -- результатом выполнения http-запроса. 13 | Прочитать из него тело ответа можно с помощью `Response.Body`, реализующего [io.ReadCloser](https://pkg.go.dev/io#ReadCloser). 14 | 15 | При работе с `http.Response` нужно помнить про пару нюансов: 16 | - тело запроса `Response.Body` обязательно нужно закрывать, иначе соединение не вернется в пул и данные запроса не освободятся 17 | - тело запроса `Response.Body` нужно всегда читать до конца (даже когда само тело не нужно) по тем же причинам 18 | 19 | Пример: 20 | 21 | ```go 22 | client := http.DefaultClient 23 | resp, err := client.Get("https://google.com") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer resp.Body.Close() 28 | 29 | data, err := io.ReadAll(resp.Body) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | fmt.Println(string(data)) 34 | ``` 35 | 36 | Вам могут пригодиться: 37 | - [sync.WaitGroup](https://pkg.go.dev/sync#WaitGroup) 38 | - [io.ReadAll](https://pkg.go.dev/io#ReadAll) 39 | -------------------------------------------------------------------------------- /tasks/03/courses/README.md: -------------------------------------------------------------------------------- 1 | # Выбор курсов 2 | 3 | Студенты должны выбрать курс, но так как возможности преподавателей ограничены, 4 | каждый из курсов имеет ограниченное количество мест. Студентов принимают на курс по следующим правилам: 5 | - все студенты сортируются по убыванию среднего балла, затем по возрастанию фамилии и имени 6 | - каждый студент зачисляется на самый высокий из своих приоритетов курс, для которого еще остались места 7 | - студенты, для которых не нашлось ни одного подходящего курса, не попадают никуда :( 8 | 9 | Реализуйте функцию `ProcessApplications(students []StudentApplication, courses []Course) map[CourseName][]StudentName`, 10 | которая по слайсу студентов с приоритетами и слайсу доступных курсов по вышеописанным правилам распределяет студентов на курсы. 11 | Содержимое структур `StudentApplication` и `Course` вы можете изучить в файле задачи. 12 | 13 | Функция должна вернуть хэш-таблицу со слайсами имен студентов `[]StudentName`, 14 | поступивших на курс с названием `CourseName`. 15 | **Важно: слайсы `[]StudentName` должны быть отсортированы по возрастанию имени и фамилии.** 16 | 17 | Пример простейшей сортировки слайса структур по возрастанию строкового поля: 18 | ```go 19 | package main 20 | 21 | import ( 22 | "cmp" 23 | "slices" 24 | ) 25 | 26 | type A struct { 27 | B string 28 | } 29 | 30 | func main() { 31 | sliceToSort := []A{ 32 | ... 33 | } 34 | 35 | slices.SortFunc(sliceToSort, func(a, b A) int { 36 | return cmp.Compare(a.B, b.B) 37 | }) 38 | 39 | fmt.Println(sliceToSort) 40 | } 41 | 42 | ``` 43 | 44 | Вам могут пригодиться: 45 | - [slices.SortFunc](https://go.dev/pkg/slices#SortFunc) 46 | - [cmp.Compare](https://pkg.go.dev/cmp#Compare) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= 5 | github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 6 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 9 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 15 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 16 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 17 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /tasks/05/dining/fork_test.go: -------------------------------------------------------------------------------- 1 | package dining 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "runtime" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestAcquireRelease(t *testing.T) { 14 | f := NewFork() 15 | 16 | f.Acquire() 17 | f.Release() 18 | } 19 | 20 | func TestSequentialAcquireRelease(t *testing.T) { 21 | f := NewFork() 22 | 23 | f.Acquire() 24 | f.Release() 25 | 26 | f.Acquire() 27 | f.Release() 28 | } 29 | 30 | func TestNoSharedState(t *testing.T) { 31 | f1 := NewFork() 32 | f1.Acquire() 33 | 34 | f2 := NewFork() 35 | f2.Acquire() 36 | 37 | f2.Release() 38 | f1.Release() 39 | } 40 | 41 | func TestMutualExlusion(t *testing.T) { 42 | f := NewFork() 43 | l := false 44 | 45 | go func() { 46 | f.Acquire() 47 | l = true 48 | time.Sleep(time.Second * 3) 49 | l = false 50 | f.Release() 51 | }() 52 | 53 | time.Sleep(time.Second) 54 | f.Acquire() 55 | assert.False(t, l) 56 | f.Release() 57 | } 58 | 59 | func TestNoBusyWait(t *testing.T) { 60 | fork := NewFork() 61 | fork.Acquire() 62 | defer fork.Release() 63 | 64 | for i := 0; i < 100; i++ { 65 | go func() { 66 | fork.Acquire() 67 | defer fork.Release() 68 | }() 69 | } 70 | 71 | verifyNoBusyGoroutines(t) 72 | } 73 | 74 | func verifyNoBusyGoroutines(t *testing.T) { 75 | time.Sleep(time.Millisecond * 100) 76 | 77 | for i := 0; i < 100; i++ { 78 | time.Sleep(time.Millisecond) 79 | 80 | var stacks []byte 81 | for n := 1 << 20; true; n *= 2 { 82 | stacks = make([]byte, n) 83 | m := runtime.Stack(stacks, true) 84 | 85 | if m < n { 86 | stacks = stacks[:m] 87 | break 88 | } 89 | } 90 | 91 | busy := bytes.Count(stacks, []byte("[running]")) 92 | busy += bytes.Count(stacks, []byte("[runnable]")) 93 | busy += bytes.Count(stacks, []byte("[sleep]")) 94 | 95 | if !assert.Less(t, busy, 2) { 96 | _, _ = os.Stderr.Write(stacks) 97 | break 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tasks/06/mockhelper/README.md: -------------------------------------------------------------------------------- 1 | # Mock Helper 2 | 3 | Для подмены поведения произвольных интерфейсов часто используют моки, сгенерированные автоматически. 4 | В этой задаче вместо генератора моков напишем библиотеку, которая поможет писать их вручную. Библиотека должна позволять: 5 | 6 | 1. Задавать ожидаемые вызовы методов с конкретными аргументами 7 | 2. Возвращать заданные значения для этих вызовов 8 | 3. Проверять, что все ожидаемые вызовы были выполнены 9 | 4. Поддерживать подачу произвольных значений (если мы не уверены, с каким аргументом будет вызван метод, или если это не важно) 10 | 11 | ## Пример использования 12 | 13 | ```go 14 | func TestUserService(t *testing.T) { 15 | mh := mockhelper.NewMockHelper(t) 16 | 17 | // Необязательно, должен быть вызван в любом случае! 18 | defer mh.Verify() 19 | 20 | mh.ExpectCall("GetUser", 123).Return(&User{Name: "Alice"}, nil) 21 | 22 | // Ожидаем вызов DeleteUser с любым аргументом 23 | mh.ExpectCall("DeleteUser", mockhelper.Any()).Return(nil) 24 | 25 | service := NewUserService(&MockDB{mh: mh}) 26 | 27 | user, err := service.GetUser(123) 28 | require.NoError(t, err) 29 | 30 | err = service.DeleteUser(456) 31 | require.NoError(t, err) 32 | } 33 | ``` 34 | 35 | Больше примеров можно найти в тестах. 36 | 37 | 38 | Для проверки того, что все ожидаемые вызовы были совершены, можно запускать `defer mh.Verify()` после каждого создания нового хелпера, но об этом можно забыть. 39 | Чтобы избавиться от надобности вызывать defer в тестах, можно воспользоваться методом `Cleanup()` у `*testing.T`, который сам вызовет нужную функцию перед завершением теста. 40 | 41 | Если во время теста был вызван неожиданный метод, нужно сразу завершать этот тест, так как мы не знаем, что должен возвращать метод, который мы не ожидали увидеть. 42 | 43 | Вам могут пригодиться: 44 | - [reflect.DeepEqual](https://pkg.go.dev/reflect#DeepEqual) 45 | - [testing.T.Cleanup](https://pkg.go.dev/testing#T.Cleanup) 46 | - [testing.T.Errorf](https://pkg.go.dev/testing#T.Errorf) 47 | - [testing.T.FailNow](https://pkg.go.dev/testing#T.FailNow) 48 | -------------------------------------------------------------------------------- /tasks/04/rw/rw_test.go: -------------------------------------------------------------------------------- 1 | package rw 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestCountReader(t *testing.T) { 14 | cr := NewCountReader() 15 | buf := make([]byte, 10) 16 | 17 | n, err := cr.Read(buf) 18 | require.NoError(t, err) 19 | assert.Equal(t, 10, n) 20 | expected := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 21 | assert.Equal(t, expected, buf) 22 | 23 | n, err = cr.Read(buf[:3]) 24 | require.NoError(t, err) 25 | assert.Equal(t, 3, n) 26 | assert.Equal(t, []byte{0, 1, 2}, buf[:3]) 27 | 28 | n, err = cr.Read(buf[:2]) 29 | require.NoError(t, err) 30 | assert.Equal(t, 2, n) 31 | assert.Equal(t, []byte{3, 4}, buf[:2]) 32 | } 33 | 34 | func TestLimitReader(t *testing.T) { 35 | sr := strings.NewReader("Hello, World!") 36 | lr := NewLimitReader(sr, 5) 37 | 38 | data, err := io.ReadAll(lr) 39 | require.Error(t, err) 40 | assert.ErrorIs(t, err, ErrLimitExceeded) 41 | assert.Equal(t, "Hello", string(data)) 42 | 43 | sr = strings.NewReader("short") 44 | lr = NewLimitReader(sr, 6) 45 | 46 | data, err = io.ReadAll(lr) 47 | require.NoError(t, err) 48 | assert.Equal(t, "short", string(data)) 49 | } 50 | 51 | func TestConcatReader(t *testing.T) { 52 | sr1 := strings.NewReader("Hello, ") 53 | sr2 := strings.NewReader("World!") 54 | cr := NewConcatReader(sr1, sr2) 55 | 56 | data, err := io.ReadAll(cr) 57 | require.NoError(t, err) 58 | assert.Equal(t, "Hello, World!", string(data)) 59 | } 60 | 61 | func TestHexWriter(t *testing.T) { 62 | var buf bytes.Buffer 63 | hw := NewHexWriter(&buf) 64 | 65 | n, err := hw.Write([]byte("Hello")) 66 | require.NoError(t, err) 67 | assert.Equal(t, 10, n) 68 | assert.Equal(t, "48656c6c6f", buf.String()) 69 | } 70 | 71 | func TestTeeWriter(t *testing.T) { 72 | var buf1, buf2 bytes.Buffer 73 | tw := NewTeeWriter(&buf1, &buf2) 74 | 75 | n, err := tw.Write([]byte("Hello")) 76 | require.NoError(t, err) 77 | assert.Equal(t, 5, n) 78 | assert.Equal(t, "Hello", buf1.String()) 79 | assert.Equal(t, "Hello", buf2.String()) 80 | } 81 | -------------------------------------------------------------------------------- /tasks/04/jsoncensor/README.md: -------------------------------------------------------------------------------- 1 | # JSON censor 2 | 3 | JSON-сообщения в Go обычно представляются в виде слайса байт. 4 | По каким-то причинам, мы не хотим, чтобы те или иные подстроки оказывались в `json`. Необходимо пройтись по всем строковым значениям в `json`, и если в значении содержится заданная подстрока, его нужно заменить на `"***"` и вернуть измененную последовательность байт: 5 | ```go 6 | input := []byte(`{"k": "v", "another_key": {"arr": ["word_to_censor"], 7 | "inner_key": "what if i have a word_to_censor here???"}}`) 8 | wordToCensor := "word_to_censor" 9 | 10 | res, _ := CensorJSON(input, wordToCensor) 11 | fmt.Println(string(res)) 12 | // {"k": "v", "another_key": {"arr": ["***"], "inner_key": "***"}} -- заменяется не только сама подстрока, а все строковое значение 13 | ``` 14 | 15 | Подобную задачу, возможно, можно решить с помощью регулярных выражений или ручного парсинга JSON-сообщения, но скорее всего это приведет к невероятно переусложненному коду. 16 | Для решения этой задачи можно вспомнить, что могут представлять собой JSON-значения: 17 | - Числа - `int` и `double` 18 | - Строки - `string` 19 | - Массивы произвольных JSON-значений - `[]any` 20 | - Наборы пар ключ-значение, где ключи являются строками - `map[string]any` 21 | 22 | Для каждого из значений понятно, что с ним нужно делать - не трогать числа, заменять строки, итерироваться по массивам и рекурсивно заменять строки в их элементах, итерироваться по парам ключ-значение и рекурсивно заменять строки в значениях. 23 | 24 | Получается, что если мы получим какой-то тип, который позволит проверять, что конкретно в нем лежит. 25 | Очень удобно, что интерфейсы в Go как раз обладают таким свойством (в частности `interface{}`) с помощью [type switch](https://go.dev/tour/methods/16). 26 | 27 | Также удобно, что JSON-сообщения можно парсить и сохранять в переменные с типом `interface{}`: 28 | ```go 29 | var val interface{} 30 | err := json.Unmarshal(jsonMessage, &val) 31 | ``` 32 | 33 | Всех этих данных должно хватить, чтобы написать функцию `CensorJSON`. 34 | 35 | Вам могут пригодиться: 36 | - [json.Unmarshal](https://pkg.go.dev/encoding/json#Unmarshal) 37 | - [json.Marshal](https://pkg.go.dev/encoding/json#Marshal) 38 | -------------------------------------------------------------------------------- /tasks/04/rw/README.md: -------------------------------------------------------------------------------- 1 | # Read and Write 2 | 3 | Потоковые чтение и запись очень часто встречаются в Go: 4 | - https://pkg.go.dev/os#File.ReadFrom 5 | - https://pkg.go.dev/net/http#Response 6 | - https://pkg.go.dev/net/http#Header.Write 7 | - https://pkg.go.dev/compress/gzip#Writer 8 | 9 | 10 | Попробуйте потренироваться в реализации потоковых чтений и записей с помощью нескольких небольших реализаций. 11 | 12 | ## CountReader 13 | 14 | `CountReader` -- бесконечный поток байт, который возвращает байты от 0 до 9: 15 | ```go 16 | r := rw.NewCountReader() 17 | p := make([]byte, 3) 18 | _, _ = r.Read(p) // p contains [0, 1, 2] 19 | _, _ = r.Read(p) // p contains [3, 4, 5] 20 | ... 21 | _, _ = r.Read(p) // p contains [9, 0, 1] 22 | ... 23 | ``` 24 | 25 | ## LimitReader 26 | 27 | `LimitReader` должен оборачивать другой `io.Reader` и возвращать ошибку `ErrLimitExceeded` в случае, если было прочитано больше n байт. 28 | 29 | ## ConcatReader 30 | 31 | `ConcatReader` должен конкатенировать потоки байт из произвольного числа `io.Reader`. Когда n-ый `io.Reader` заканчивается, `ConcatReader` должен продолжать писать в переданный ему слайс, пока в нем осталось место: 32 | 33 | ```go 34 | r1 := strings.NewReader("a") 35 | r2 := strings.NewReader("bcdef") 36 | cr := rw.NewConcatReader(r1, r2) 37 | p := make([]byte, 3) 38 | _, _ = cr.Read(p) // p contains abc 39 | ``` 40 | 41 | ## HexWriter 42 | `HexWriter` должен оборачивать другой `io.Writer` и записывать в него поток байт, который перед этим кодируется в hex 43 | 44 | ## TeeWriter 45 | `TeeWriter` должен принимать произвольное количество `io.Writer` и мультиплексировать в них поступающий поток байт. Если в процессе одной из записей произойдет ошибка, `TeeWriter` должен перестать писть в последующие `io.Writer`. 46 | 47 | ```go 48 | tr := rw.NewTeeWriter(os.Stdout, os.Stderr) 49 | _, _ = fmt.Fprintf(tr, "Hello, World!") 50 | ``` 51 | 52 | Для реализации рекомендуется изучить контракты, которым должны следовать интерфейсы [io.Reader](https://pkg.go.dev/io#Reader) и [io.Writer](https://pkg.go.dev/io#Writer). 53 | 54 | В решении могут пригодиться: 55 | - [hex.EncodingLen](https://pkg.go.dev/encoding/hex#EncodedLen) 56 | - [hex.Encode](https://pkg.go.dev/encoding/hex#Encode) 57 | -------------------------------------------------------------------------------- /tasks/03/pr/README.md: -------------------------------------------------------------------------------- 1 | # Утилита `pr` 2 | 3 | Вам необходимо реализовать подобие GNU-утилиты `pr`, 4 | которая занимается пагинацией файлов. 5 | Утилита принимает на вход произвольное количество файлов и ключ `-n`, 6 | определяющий, сколько строк файла будет на каждой странице. 7 | 8 | Формат страницы выглядит так: 9 | - Первая строка содержит имя файла (в том виде, в котором он пришел в утилиту) и номер страницы (нумерация с нуля). Имя файла и номер страницы разделены символом табуляции `\t`: `./some/file/path\tPage: 0` 10 | - После первой строки следуют два переноса строки `\n` 11 | - После переносов следуют `n` строк файла 12 | - Если `n` оказалось больше количества оставшихся строк, то дополнительно добавляются `n - кол-во строк в файле` переносов строки `\n` 13 | - В конце страницы находятся 4 переноса строки `\n` 14 | 15 | Пример: 16 | Файл `./a.txt`: 17 | ``` 18 | a 19 | b 20 | c 21 | ``` 22 | 23 | Команда запуска: 24 | ```shell 25 | ./gopr -n 2 ./a.txt 26 | ``` 27 | 28 | Вывод: 29 | ``` 30 | ./a.txt Page: 0 31 | 32 | a 33 | b 34 | 35 | 36 | 37 | ./a.txt Page: 1 38 | 39 | c 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | Если на вход утилиты пришло больше одного файла, не нужно смешивать содержимое файлов, 48 | необходимо просто вывести страничное представлление для всех файлов по порядку. 49 | 50 | Вам нужно будет написать консольную утилиту с нуля: 51 | вам дан пустой файл `main.go` в директории `cmd/gopr`. 52 | Для сборки утилиты используйте команду (находясь в директории задачи): 53 | ```shell 54 | go build -o ./gopr ./cmd/gopr/... 55 | ``` 56 | 57 | Для парсинга аргументов командной строки предлагается использовать 58 | пакет [flag](https://pkg.go.dev/flag) из стандартной библиотеки Go. Ознакомьтесь с его содержимым. 59 | 60 | Если на вход утилите подали несуществующий файл или в процессе пагинации произошла ошибка, 61 | вам необходимо завершить процесс с кодом 1. 62 | 63 | Вам могут пригодиться: 64 | - [os.Open](https://pkg.go.dev/os#Open) 65 | - [os.ReadFile](https://pkg.go.dev/os#ReadFile) 66 | - [bytes.Reader](https://pkg.go.dev/bytes#Reader) 67 | - [bufio.Scanner](https://pkg.go.dev/bufio#Scanner) 68 | - [flag.Parse](https://pkg.go.dev/flag#Parse) 69 | - [flag.Args](https://pkg.go.dev/flag#Args) 70 | - [fmt.Printf](https://pkg.go.dev/fmt#Printf) 71 | - [log.Fatal](https://pkg.go.dev/log#Fatal) 72 | -------------------------------------------------------------------------------- /lectures/09/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 8. Generics 4 | 5 | ## Generics 6 | 7 | * Дженерики являются одним из замых значительных изменений в языке со времен первого open source релиза 8 | * Дженерики позволяют писать обобщенные функции и типы, избавляя от необходимости писать много дублирующегося кода для разных типов 9 | * В релиз 1.18 были добавлены: type parameters для функций и типов, определение интерфейсов как набора типов и автоматический вывод типов при вызовах 10 | 11 | ## Type parameters 12 | 13 | * Функции и типы могут теперь иметь отдельный список параметров, описывающий типы, используемые для описания аргументов или внутренностей типа 14 | * В момент вызова с такими параметрами происходит инстанцирование, состоящее из двух шагов: подстановка конкретых типов на основании аргументов и проверка компилятором совместимости конкретных типов с ограничениями 15 | * После инстанцирования получается (скрытая от нас) обычная функция или тип, что никак не влияет на производительность программы 16 | 17 | ## Type sets 18 | 19 | * Наподобие того, как обычный тип описывает множество значений агрумента, "тип" параметра-типа определяет множество типов, подходящих для аргумента 20 | * Такой мета-тип называется **type constraint**. Некоторое количетсво таких типов можно найти в пакете [constraints](https://pkg.go.dev/golang.org/x/exp/constraints) 21 | * Type constraint должен являться интерфейсом, таким образом, дженерики несколько расширяют понятие интерфейса 22 | 23 | ## Type sets 24 | 25 | * Мы можем посмотреть на интерфейс как "ограничение", описывающее множество типов, удовлетворяющих этому интерфейсу 26 | * С этой точки зрения интерфейс описывает множество типов. И в go1.18 появилась возможность явно добавлять типы в это множетсво (при этом не обязательно содержащие интерфейсы) 27 | * Таким образом `interface {int | bool | string}` описывает интерфейс, которому удовлетворяют три типа (которые, при этом, не имеют типов) 28 | * Синтаксис `~string` говорит, что в множество включен не только тип `string`, но и типы, основанные на нем 29 | * Дополнительно, появилось ключевое слово `any`, эквивалентное `interface{}` 30 | 31 | ## Type inference 32 | 33 | * Type inference позволяет опускать type parameters в большинстве случаев, автоматически выводя их из аргументов 34 | * Вывод типа может работать на основании аргументов и на основании других type constraints 35 | * Если type inference завершился неуспешно, всегда можно вручную указать список type parameters -------------------------------------------------------------------------------- /lectures/06/code/cache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var urls = []string{ 12 | "https://yandex.ru", 13 | "https://google.com", 14 | "https://yandex.ru", 15 | "https://yandex.ru", 16 | "https://google.com", 17 | "https://yandex.ru", 18 | "https://yandex.ru", 19 | "https://google.com", 20 | "https://yandex.ru", 21 | "https://yandex.ru", 22 | "https://google.com", 23 | "https://yandex.ru", 24 | "https://yandex.ru", 25 | "https://google.com", 26 | "https://yandex.ru", 27 | "https://yandex.ru", 28 | "https://google.com", 29 | "https://yandex.ru", 30 | "https://yandex.ru", 31 | "https://google.com", 32 | "https://yandex.ru", 33 | "https://yandex.ru", 34 | "https://google.com", 35 | "https://yandex.ru", 36 | "https://yandex.ru", 37 | "https://google.com", 38 | "https://yandex.ru", 39 | } 40 | 41 | func main() { 42 | b := GetBodies(urls) 43 | for _, body := range b { 44 | fmt.Println(body[:100]) 45 | } 46 | } 47 | 48 | type Entry struct { 49 | value string 50 | ready <-chan struct{} 51 | } 52 | 53 | type Cache struct { 54 | values map[string]*Entry 55 | fn func(string) string 56 | m sync.Mutex 57 | } 58 | 59 | func NewCache(fn func(string) string) *Cache { 60 | return &Cache{ 61 | values: make(map[string]*Entry), 62 | fn: fn, 63 | } 64 | } 65 | 66 | func (c *Cache) Get(key string) string { 67 | c.m.Lock() 68 | v := c.values[key] 69 | if v != nil { 70 | c.m.Unlock() 71 | <-v.ready 72 | return v.value 73 | } 74 | ready := make(chan struct{}) 75 | c.values[key] = &Entry{ready: ready} 76 | c.m.Unlock() 77 | 78 | newV := c.fn(key) 79 | 80 | c.m.Lock() 81 | c.values[key].value = newV 82 | c.m.Unlock() 83 | close(ready) 84 | 85 | return newV 86 | } 87 | 88 | func GetBodies(urls []string) []string { 89 | var res []string 90 | var cache = NewCache(func(u string) string { 91 | time.Sleep(time.Second) 92 | fmt.Println("NEW REQUEST", u) 93 | resp, err := http.Get(u) 94 | if err != nil { 95 | return "error" 96 | } 97 | data, err := ioutil.ReadAll(resp.Body) 98 | if err != nil { 99 | return "error" 100 | } 101 | return string(data) 102 | }) 103 | 104 | var results = make(chan string) 105 | for _, u := range urls { 106 | go func(u string) { 107 | results <- cache.Get(u) 108 | }(u) 109 | } 110 | for i := 0; i < len(urls); i++ { 111 | res = append(res, <-results) 112 | } 113 | return res 114 | } 115 | -------------------------------------------------------------------------------- /lectures/08/code/display/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | type Example struct { 11 | I int 12 | S []int 13 | M map[int]string 14 | Sub *Example 15 | } 16 | 17 | func main() { 18 | Display(Example{ 19 | I: 42, 20 | S: []int{1, 2, 3}, 21 | M: map[int]string{1: "one", 2: "two"}, 22 | Sub: &Example{ 23 | I: 43, 24 | }, 25 | }) 26 | fmt.Println("===") 27 | Display(os.Stderr) 28 | fmt.Println("===") 29 | var i interface{} = 3 30 | Display(&i) 31 | } 32 | 33 | func Display(v interface{}) { 34 | display("", reflect.ValueOf(v)) 35 | } 36 | 37 | func display(path string, v reflect.Value) { 38 | switch v.Kind() { 39 | case reflect.Invalid: 40 | fmt.Printf("%s = invalid\n", path) 41 | case reflect.Slice, reflect.Array: 42 | for i := 0; i < v.Len(); i++ { 43 | display(fmt.Sprintf("%s[%d]", path, i), v.Index(i)) 44 | } 45 | case reflect.Struct: 46 | for i := 0; i < v.NumField(); i++ { 47 | fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name) 48 | display(fieldPath, v.Field(i)) 49 | } 50 | case reflect.Map: 51 | for _, key := range v.MapKeys() { 52 | display(fmt.Sprintf("%s[%s]", path, 53 | formatAtom(key)), v.MapIndex(key)) 54 | } 55 | case reflect.Ptr: 56 | if v.IsNil() { 57 | fmt.Printf("%s = nil\n", path) 58 | } else { 59 | display(fmt.Sprintf("(*%s)", path), v.Elem()) 60 | } 61 | case reflect.Interface: 62 | if v.IsNil() { 63 | fmt.Printf("%s = nil\n", path) 64 | } else { 65 | fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) 66 | display(path+".value", v.Elem()) 67 | } 68 | default: // basic types, channels, funcs 69 | fmt.Printf("%s = %s\n", path, formatAtom(v)) 70 | } 71 | } 72 | 73 | func formatAtom(v reflect.Value) string { 74 | switch v.Kind() { 75 | case reflect.Invalid: 76 | return "invalid" 77 | case reflect.Int, reflect.Int8, reflect.Int16, 78 | reflect.Int32, reflect.Int64: 79 | return strconv.FormatInt(v.Int(), 10) 80 | case reflect.Uint, reflect.Uint8, reflect.Uint16, 81 | reflect.Uint32, reflect.Uint64, reflect.Uintptr: 82 | return strconv.FormatUint(v.Uint(), 10) 83 | // ...floating-point and complex cases omitted for brevity... 84 | case reflect.Bool: 85 | return strconv.FormatBool(v.Bool()) 86 | case reflect.String: 87 | return strconv.Quote(v.String()) 88 | case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map: 89 | return v.Type().String() + " 0x" + 90 | strconv.FormatUint(uint64(v.Pointer()), 16) 91 | default: // reflect.Array, reflect.Struct, reflect.Interface 92 | return "<" + v.Type().String() + " value>" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tasks/03/pr/pr_test.go: -------------------------------------------------------------------------------- 1 | package pr 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/dbeliakov/mipt-golang-course/utils/testutils" 15 | ) 16 | 17 | const binPath = "github.com/dbeliakov/mipt-golang-course/tasks/03/pr/cmd/gopr" 18 | 19 | func TestPR(t *testing.T) { 20 | testCases := []struct { 21 | file string 22 | nLines int 23 | wantErr bool 24 | }{ 25 | {file: filepath.Join("01", "simple.txt"), nLines: 4}, 26 | {file: filepath.Join("02", "multiple_pages.txt"), nLines: 4}, 27 | {file: filepath.Join("03", "add_newlines.txt"), nLines: 4}, 28 | // NOTE: this file does not exist 29 | {file: strings.Repeat("abcd", 128), wantErr: true}, 30 | } 31 | 32 | binCache := testutils.NewBinaryCache() 33 | testBinary := binCache.LoadBinary(binPath) 34 | 35 | for _, tc := range testCases { 36 | t.Run(tc.file, func(t *testing.T) { 37 | inputFile := filepath.Join("testdata", tc.file) 38 | expectedOutputFile := filepath.Join("testdata", tc.file+".output") 39 | 40 | cmd := exec.Command(testBinary, "-n", strconv.Itoa(tc.nLines), inputFile) 41 | res, err := cmd.Output() 42 | if tc.wantErr { 43 | assert.Error(t, err) 44 | } else { 45 | expectedResult, err := os.ReadFile(expectedOutputFile) 46 | require.NoError(t, err) 47 | 48 | if !assert.Equal(t, string(expectedResult), string(res)) { 49 | fileNameToDumpResult := inputFile + ".actual" 50 | err = os.WriteFile(fileNameToDumpResult, res, 0644) 51 | require.NoError(t, err) 52 | 53 | t.Logf("Actual result has been written to %q", fileNameToDumpResult) 54 | } 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestPR_MultipleFiles(t *testing.T) { 61 | args := []string{ 62 | "-n", "6", 63 | filepath.Join("testdata", "multiple_files", "a.txt"), 64 | filepath.Join("testdata", "multiple_files", "b.txt"), 65 | filepath.Join("testdata", "multiple_files", "c.txt"), 66 | } 67 | expectedOutputFile := filepath.Join("testdata", "multiple_files", "expected.txt") 68 | 69 | binCache := testutils.NewBinaryCache() 70 | testBinary := binCache.LoadBinary(binPath) 71 | 72 | cmd := exec.Command(testBinary, args...) 73 | res, err := cmd.Output() 74 | require.NoError(t, err) 75 | 76 | expectedResult, err := os.ReadFile(expectedOutputFile) 77 | require.NoError(t, err) 78 | 79 | if !assert.Equal(t, string(expectedResult), string(res)) { 80 | fileNameToDumpResult := filepath.Join("testdata", "multiple_files", "actual.txt") 81 | err = os.WriteFile(fileNameToDumpResult, res, 0644) 82 | require.NoError(t, err) 83 | 84 | t.Logf("Actual result has been written to %q", fileNameToDumpResult) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tasks/03/ini/README.md: -------------------------------------------------------------------------------- 1 | # Простой парсер ini-файлов 2 | 3 | ``` 4 | [section1] 5 | key = value 6 | another_key = value 7 | 8 | [section2] 9 | new_key = old_value 10 | 11 | [empty_section] 12 | ``` 13 | 14 | ini-файлы имеют очень простой, но нестандартизированный формат. 15 | Давайте опишем его формат таким образом: 16 | - Файл состоит из пустых строк, строк с заголовками секций, и строк с парами ключ-значение. Любые другие строки считаются невалидными. 17 | - Пустая строка может содержать произвольное количество пробельных символов. 18 | - Имена заголовков секций находятся внутри квадратных скобок. До и после скобок может быть произвольное количество пробельных символов. 19 | - Имена заголовков должны иметь ненулевую длину 20 | - Пары ключ-значение разделены символом `=`. До и после символа `=` может находиться произвольное количество пробельных символов. 21 | - Имена ключей и значений не могут содержать в себе символ `=` 22 | - Ключи в секциях могут повторяться, нужно оставить в результате последнее значение, присвоенное ключу. 23 | - Имена секций могут повторяться, если секция повторяется несколько раз, нужно объединить все повторения секции. 24 | 25 | 26 | Реализуйте функцию `Parse(fileName string) (map[string]map[string]string, error)`, 27 | которая парсит ini-файл по пути `fileName` и возвращает хэш-таблицу с наборами ключ-значение (также в хэш-таблице), 28 | соответствующими именам секций. Пример результата для фрагмента ini-файла, расположенного выше: 29 | ```go 30 | map[string]map[string]string{ 31 | "section1": { 32 | "key": "value", 33 | "another_key": "value", 34 | }, 35 | "section2": { 36 | "new_key": "old_value" 37 | }, 38 | } 39 | ``` 40 | 41 | В случае, если формат файла не соответствует описанным нами требованиям, 42 | вам необходимо вернуть ошибку `ErrFileIsMalformed`. 43 | Примеры невалидных файлов вы можете найти в тестах. 44 | 45 | Для парсинга файла вам необходимо будет для начала прочитать его, 46 | а также, возможно, придется итерироваться по строкам файла. 47 | Ниже описан один из способов прочитать и вывести все строки из файла: 48 | 49 | ```go 50 | package main 51 | 52 | import ( 53 | "bufio" 54 | "fmt" 55 | "log" 56 | "os" 57 | ) 58 | 59 | const filePath = "some/file" 60 | 61 | func PrintFile(fileName string) error { 62 | f, err := os.Open(fileName) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | s := bufio.NewScanner(f) 68 | for s.Scan() { 69 | fmt.Println(s.Text()) 70 | } 71 | 72 | err = s.Err() 73 | if err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func main() { 80 | err := PrintFile(filePath) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | } 85 | ``` 86 | 87 | Вам могут пригодиться: 88 | - [os.Open](https://pkg.go.dev/os#Open) 89 | - [os.ReadFile](https://pkg.go.dev/os#ReadFile) 90 | - [bytes.Reader](https://pkg.go.dev/bytes#Reader) 91 | - [bufio.Scanner](https://pkg.go.dev/bufio#Scanner) 92 | -------------------------------------------------------------------------------- /tasks/06/genericutils/utils_test.go: -------------------------------------------------------------------------------- 1 | package genericutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | t.Run("filter even numbers", func(t *testing.T) { 12 | nums := []int{1, 2, 3, 4, 5} 13 | even := Filter(nums, func(n int) bool { return n%2 == 0 }) 14 | assert.Equal(t, []int{2, 4}, even) 15 | }) 16 | 17 | t.Run("filter empty slice", func(t *testing.T) { 18 | var empty []string 19 | result := Filter(empty, func(s string) bool { return len(s) > 0 }) 20 | assert.Empty(t, result) 21 | }) 22 | } 23 | 24 | func TestGroupBy(t *testing.T) { 25 | type person struct { 26 | Name string 27 | Age int 28 | } 29 | 30 | t.Run("group by age", func(t *testing.T) { 31 | people := []person{ 32 | {"Alice", 25}, 33 | {"Bob", 25}, 34 | {"Charlie", 30}, 35 | } 36 | groups := GroupBy(people, func(p person) int { return p.Age }) 37 | expected := map[int][]person{ 38 | 25: {{"Alice", 25}, {"Bob", 25}}, 39 | 30: {{"Charlie", 30}}, 40 | } 41 | assert.Equal(t, expected, groups) 42 | }) 43 | 44 | t.Run("empty input", func(t *testing.T) { 45 | var empty []person 46 | groups := GroupBy(empty, func(p person) string { return p.Name }) 47 | assert.Empty(t, groups) 48 | }) 49 | } 50 | 51 | func TestMaxBy(t *testing.T) { 52 | t.Run("max by length", func(t *testing.T) { 53 | words := []string{"a", "aa", "aaa"} 54 | max := MaxBy(words, func(a, b string) bool { return len(a) < len(b) }) 55 | assert.Equal(t, "aaa", max) 56 | }) 57 | 58 | t.Run("empty slice returns zero value", func(t *testing.T) { 59 | var empty []int 60 | result := MaxBy(empty, func(a, b int) bool { return a < b }) 61 | assert.Zero(t, result) 62 | }) 63 | } 64 | 65 | func TestRepeat(t *testing.T) { 66 | t.Run("repeat value", func(t *testing.T) { 67 | result := Repeat("x", 3) 68 | assert.Equal(t, []string{"x", "x", "x"}, result) 69 | }) 70 | 71 | t.Run("zero times", func(t *testing.T) { 72 | result := Repeat(1, 0) 73 | assert.Empty(t, result) 74 | }) 75 | } 76 | 77 | func TestJSONParse(t *testing.T) { 78 | t.Run("parse valid JSON", func(t *testing.T) { 79 | type data struct { 80 | Name string `json:"name"` 81 | Age int `json:"age"` 82 | } 83 | jsonStr := []byte(`{"name":"Alice","age":25}`) 84 | 85 | result, err := JSONParse[data](jsonStr) 86 | require.NoError(t, err) 87 | assert.Equal(t, data{"Alice", 25}, result) 88 | }) 89 | 90 | t.Run("parse invalid JSON", func(t *testing.T) { 91 | _, err := JSONParse[int]([]byte("invalid")) 92 | require.Error(t, err) 93 | }) 94 | } 95 | 96 | func TestDedup(t *testing.T) { 97 | t.Run("deduplicate integers", func(t *testing.T) { 98 | nums := []int{1, 2, 2, 3, 3, 3} 99 | unique := Dedup(nums) 100 | assert.ElementsMatch(t, []int{1, 2, 3}, unique) 101 | }) 102 | 103 | t.Run("deduplicate strings", func(t *testing.T) { 104 | words := []string{"a", "b", "a", "c"} 105 | unique := Dedup(words) 106 | assert.ElementsMatch(t, []string{"a", "b", "c"}, unique) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /tasks/02/rle/README.md: -------------------------------------------------------------------------------- 1 | # RLE-сжатие 2 | 3 | Реализуйте функцию `RLECompress`, которая производит RLE-сжатие строки: на вход подается строка, состоящая из часто повторяющихся символов латинского алфавита (a-z, A-Z). Функция должна вернуть строку, в которой все идущие подряд одинаковые символы заменены на один экземпляр символа и количество повторений: 4 | `aabbc -> a2b2c1`. 5 | 6 | В данной задаче есть бенчмарк, проверяющий, насколько эффективна ваша реализация алгоритма. Для того, чтобы не выделять лишнюю память, изучите [`strings.Builder`](https://pkg.go.dev/strings#Builder). 7 | 8 | Результат бенчмарка авторского решения: 9 | ``` 10 | goos: darwin 11 | goarch: arm64 12 | pkg: github.com/dbeliakov/mipt-golang-course/tasks/02/rle 13 | BenchmarkRLECompress 14 | BenchmarkRLECompress-12 226419 4500 ns/op 5368 B/op 10 allocs/op 15 | PASS 16 | ``` 17 | 18 | # Запуск бенчмарков и анализ профилей 19 | 20 | Для запуска бенчмарков с профайлером на примере задачи rle можно использовать следующую команду: 21 | ```shell 22 | go test -v -run='^$' -bench='.*' -memprofile=mem.out ./tasks/02/rle/... 23 | ``` 24 | 25 | Семплы профайлера будут находиться в `mem.out`. Анализировать полученные семплы удобно с помощью встроенной утилиты `pprof`: 26 | 27 | ```shell 28 | go tool pprof mem.out 29 | ``` 30 | ``` 31 | File: rle.test 32 | Type: alloc_space 33 | Time: Jan 25, 2025 at 4:17pm (MSK) 34 | Entering interactive mode (type "help" for commands, "o" for options) 35 | (pprof) 36 | ``` 37 | 38 | Утилита запускает нтерактивную среду, в которой можно анализировать полученные профили, используя различные команды. Чтобы узнать, какие команды есть у `pprof`, нужно вызвать `help`. Для того, чтобы узнать, как вызывать конкретную команду, нужно вызвать `help [имя команды]`. 39 | 40 | ## Полезные команды 41 | 42 | Команда `top` покажет, какие функции выделяли больше всего памяти: 43 | ``` 44 | (pprof) top -cum 45 | Showing nodes accounting for 22.30GB, 100% of 22.30GB total 46 | Dropped 1 node (cum <= 0.11GB) 47 | flat flat% sum% cum cum% 48 | 0 0% 0% 22.30GB 100% github.com/dbeliakov/mipt-golang-course/tasks/02/rle.BenchmarkRLECompress 49 | 22.30GB 100% 100% 22.30GB 100% github.com/dbeliakov/mipt-golang-course/tasks/02/rle.RLECompress (inline) 50 | 0 0% 100% 22.30GB 100% testing.(*B).runN 51 | 0 0% 100% 22.30GB 100% testing.(*B).launch 52 | ``` 53 | 54 | Для того, чтобы проанализировать функцию, которая делает слишком много аллокаций, можно использовать команду `list`, которая покажет, на каких строчках происходили аллокации: 55 | ``` 56 | (pprof) list github.com/dbeliakov/mipt-golang-course/tasks/02/rle.RLECompress 57 | Total: 22.30GB 58 | ROUTINE ======================== github.com/dbeliakov/mipt-golang-course/tasks/02/rle.RLECompress in /-S/mipt-golang-course/tasks/02/rle/rle.go 59 | 22.30GB 22.30GB (flat, cum) 100% of Total 60 | . . 31:func RLECompress(input string) string { 61 | . . 32: // return input + input 62 | . . 33: for _, c := range input { 63 | 22.30GB 22.30GB 34: input += string(c) 64 | . . 35: } 65 | . . 36: 66 | . . 37: return input 67 | . . 38:} 68 | ``` 69 | -------------------------------------------------------------------------------- /tasks/06/formvalues/marshal_test.go: -------------------------------------------------------------------------------- 1 | package formvalues 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type Person struct { 13 | Name string `form:"name"` 14 | Age int `form:"age"` 15 | Active bool `form:"active"` 16 | Hobbies []string `form:"hobbies"` 17 | } 18 | 19 | type Simple struct { 20 | Title string 21 | Count int 22 | } 23 | 24 | func TestUnpack(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | form url.Values 28 | target interface{} 29 | want interface{} 30 | wantErr bool 31 | }{ 32 | { 33 | name: "all basic types", 34 | form: url.Values{ 35 | "name": {"Alice"}, 36 | "age": {"30"}, 37 | "active": {"true"}, 38 | }, 39 | target: &Person{}, 40 | want: &Person{ 41 | Name: "Alice", 42 | Age: 30, 43 | Active: true, 44 | }, 45 | }, 46 | { 47 | name: "string slice", 48 | form: url.Values{ 49 | "hobbies": {"reading", "coding"}, 50 | }, 51 | target: &Person{}, 52 | want: &Person{ 53 | Hobbies: []string{"reading", "coding"}, 54 | }, 55 | }, 56 | { 57 | name: "struct tags override", 58 | form: url.Values{ 59 | "name": {"Bob"}, 60 | "age": {"25"}, 61 | }, 62 | target: &Person{}, 63 | want: &Person{ 64 | Name: "Bob", 65 | Age: 25, 66 | }, 67 | }, 68 | { 69 | name: "default field names", 70 | form: url.Values{ 71 | "title": {"Test"}, 72 | "count": {"42"}, 73 | }, 74 | target: &Simple{}, 75 | want: &Simple{ 76 | Title: "Test", 77 | Count: 42, 78 | }, 79 | }, 80 | { 81 | name: "invalid int", 82 | form: url.Values{ 83 | "age": {"thirty"}, 84 | }, 85 | target: &Person{}, 86 | wantErr: true, 87 | }, 88 | { 89 | name: "invalid bool", 90 | form: url.Values{ 91 | "active": {"yes"}, 92 | }, 93 | target: &Person{}, 94 | wantErr: true, 95 | }, 96 | { 97 | name: "unsupported type", 98 | form: url.Values{ 99 | "title": {"Test"}, 100 | }, 101 | target: &struct{ Title float64 }{}, 102 | wantErr: true, 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | req := &http.Request{Form: tt.form} 109 | err := Unpack(req, tt.target) 110 | 111 | if tt.wantErr { 112 | require.Error(t, err, "Unpack() should fail") 113 | return 114 | } 115 | 116 | require.NoError(t, err, "Unpack() should succeed") 117 | assert.Equal(t, tt.want, tt.target, "Unpack() result mismatch") 118 | }) 119 | } 120 | } 121 | 122 | func TestUnpack_EdgeCases(t *testing.T) { 123 | t.Run("empty form", func(t *testing.T) { 124 | var p Person 125 | req := &http.Request{Form: url.Values{}} 126 | require.NoError(t, Unpack(req, &p)) 127 | assert.Equal(t, Person{}, p) 128 | }) 129 | 130 | t.Run("nil target", func(t *testing.T) { 131 | req := &http.Request{Form: url.Values{"name": {"Alice"}}} 132 | err := Unpack(req, nil) 133 | require.Error(t, err) 134 | }) 135 | 136 | t.Run("non-pointer target", func(t *testing.T) { 137 | req := &http.Request{Form: url.Values{"name": {"Alice"}}} 138 | var p Person 139 | err := Unpack(req, p) 140 | require.Error(t, err) 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /tasks/06/mockhelper/mock_helper_test.go: -------------------------------------------------------------------------------- 1 | package mockhelper_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/dbeliakov/mipt-golang-course/tasks/06/mockhelper" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // ---------------------------------- 14 | 15 | type DB interface { 16 | GetUserIDs(context.Context) ([]int, error) 17 | DeleteUserByID(context.Context, int) error 18 | } 19 | 20 | var _ DB = &mockDB{} 21 | 22 | type mockDB struct { 23 | mh mockhelper.MockHelper 24 | } 25 | 26 | func NewMockDB(mh mockhelper.MockHelper) *mockDB { 27 | return &mockDB{ 28 | mh: mh, 29 | } 30 | } 31 | 32 | func (m *mockDB) GetUserIDs(ctx context.Context) ([]int, error) { 33 | returnValues := m.mh.Call("GetUserIDs", ctx) 34 | 35 | var ret0 []int 36 | if returnValues[0] != nil { 37 | ret0 = returnValues[0].([]int) 38 | } 39 | var ret1 error 40 | if returnValues[1] != nil { 41 | ret1 = returnValues[1].(error) 42 | } 43 | 44 | return ret0, ret1 45 | } 46 | 47 | func (m *mockDB) DeleteUserByID(ctx context.Context, id int) error { 48 | retVal := m.mh.Call("DeleteUserByID", ctx, id) 49 | 50 | var ret error 51 | if retVal[0] != nil { 52 | ret = retVal[0].(error) 53 | } 54 | return ret 55 | } 56 | 57 | // ---------------------------------- 58 | 59 | func DeleteAllUsers(ctx context.Context, db DB) []error { 60 | users, err := db.GetUserIDs(ctx) 61 | if err != nil { 62 | return []error{err} 63 | } 64 | 65 | var errs []error 66 | for _, id := range users { 67 | err = db.DeleteUserByID(ctx, id) 68 | if err != nil { 69 | errs = append(errs, err) 70 | } 71 | } 72 | 73 | if len(errs) > 0 { 74 | return errs 75 | } 76 | return nil 77 | } 78 | 79 | func TestDeleteAllUsers(t *testing.T) { 80 | ctx := context.Background() 81 | mh := mockhelper.NewMockHelper(t) 82 | db := NewMockDB(mh) 83 | 84 | expectIDs := []int{1, 2, 3} 85 | mh.ExpectCall("GetUserIDs", ctx).Return(expectIDs, nil) 86 | for _, id := range expectIDs { 87 | mh.ExpectCall("DeleteUserByID", ctx, id).Return(nil) 88 | } 89 | 90 | errs := DeleteAllUsers(ctx, db) 91 | assert.Empty(t, errs) 92 | } 93 | 94 | func TestDeleteAllUsers_GetIDsError(t *testing.T) { 95 | ctx := context.Background() 96 | mh := mockhelper.NewMockHelper(t) 97 | db := NewMockDB(mh) 98 | 99 | expectErr := errors.New("oops") 100 | mh.ExpectCall("GetUserIDs", ctx).Return(nil, expectErr) 101 | 102 | errs := DeleteAllUsers(ctx, db) 103 | require.Len(t, errs, 1) 104 | assert.Equal(t, expectErr, errs[0]) 105 | } 106 | 107 | func TestDeleteAllUsers_DeleteErrors(t *testing.T) { 108 | ctx := context.Background() 109 | mh := mockhelper.NewMockHelper(t) 110 | db := NewMockDB(mh) 111 | 112 | expectIDs := []int{1, 2, 3} 113 | mh.ExpectCall("GetUserIDs", ctx).Return(expectIDs, nil) 114 | expectErr := errors.New("something went wrong") 115 | for _, id := range expectIDs { 116 | if id == 2 { 117 | mh.ExpectCall("DeleteUserByID", ctx, id).Return(nil) 118 | continue 119 | } 120 | mh.ExpectCall("DeleteUserByID", ctx, id).Return(expectErr) 121 | } 122 | 123 | errs := DeleteAllUsers(ctx, db) 124 | require.Len(t, errs, 2) 125 | assert.Equal(t, expectErr, errs[0]) 126 | assert.Equal(t, expectErr, errs[1]) 127 | } 128 | 129 | func TestMockHelper_AnyWorks(t *testing.T) { 130 | ctx := context.Background() 131 | mh := mockhelper.NewMockHelper(t) 132 | db := NewMockDB(mh) 133 | 134 | for i := 0; i < 10; i++ { 135 | mh.ExpectCall("DeleteUserByID", ctx, mockhelper.Any()).Return(nil) 136 | } 137 | 138 | for i := 0; i < 10; i++ { 139 | _ = db.DeleteUserByID(ctx, i) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lectures/01/code/downloader/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | func main() { 20 | runtime.GOMAXPROCS(1) 21 | 22 | startTime := time.Now() 23 | 24 | dType := flag.String("type", "sync", "[sync|async]") 25 | dir := flag.String("dir", ".", "directory to download") 26 | flag.Parse() 27 | 28 | var ( 29 | d Downloader 30 | err error 31 | ) 32 | switch *dType { 33 | case "sync": 34 | d, err = NewSyncDownloader(*dir) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | case "async": 39 | d, err = NewAsyncDownloader(*dir) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | default: 44 | log.Fatal("invalid argument: type") 45 | } 46 | 47 | for _, url := range flag.Args() { 48 | fmt.Printf("Downloading %s\n", url) 49 | d.Download(url) 50 | } 51 | d.Wait() 52 | 53 | fmt.Printf("Completed in %s\n", time.Since(startTime)) 54 | } 55 | 56 | type Downloader interface { 57 | Download(url string) 58 | Wait() 59 | } 60 | 61 | type SyncDownloader struct { 62 | dir string 63 | httpClient *http.Client 64 | } 65 | 66 | var _ Downloader = (*SyncDownloader)(nil) 67 | 68 | func NewSyncDownloader(dir string) (*SyncDownloader, error) { 69 | err := os.MkdirAll(dir, 0755) 70 | if err != nil { 71 | return nil, fmt.Errorf("create directory: %w", err) 72 | } 73 | return &SyncDownloader{ 74 | dir: dir, 75 | httpClient: &http.Client{}, 76 | }, nil 77 | } 78 | 79 | func (d SyncDownloader) Download(url string) { 80 | err := copyContentToFile(d.httpClient, url, d.dir) 81 | if err != nil { 82 | log.Printf("download failed: %v", err) 83 | } 84 | } 85 | 86 | func (d SyncDownloader) Wait() { 87 | // NOTE: nothing to wait 88 | } 89 | 90 | type AsyncDownloader struct { 91 | dir string 92 | httpClient *http.Client 93 | wg sync.WaitGroup 94 | } 95 | 96 | var _ Downloader = (*AsyncDownloader)(nil) 97 | 98 | func NewAsyncDownloader(dir string) (*AsyncDownloader, error) { 99 | err := os.MkdirAll(dir, 0755) 100 | if err != nil { 101 | return nil, fmt.Errorf("create directory: %w", err) 102 | } 103 | return &AsyncDownloader{ 104 | dir: dir, 105 | httpClient: &http.Client{}, 106 | }, nil 107 | } 108 | 109 | func (d *AsyncDownloader) Download(url string) { 110 | d.wg.Add(1) 111 | go func() { 112 | defer d.wg.Done() 113 | err := copyContentToFile(d.httpClient, url, d.dir) 114 | if err != nil { 115 | log.Printf("download failed: %v", err) 116 | } 117 | }() 118 | } 119 | 120 | func (d *AsyncDownloader) Wait() { 121 | d.wg.Wait() 122 | } 123 | 124 | func copyContentToFile(client *http.Client, url string, dir string) error { 125 | resp, err := client.Get(url) 126 | if err != nil { 127 | return fmt.Errorf("get url content: %w", err) 128 | } 129 | defer func() { _ = resp.Body.Close() }() 130 | 131 | var out *os.File 132 | for i := 0; i < 5 && out == nil; i++ { 133 | fn := filepath.Join(dir, filepath.Base(url)) 134 | if i > 0 { 135 | ext := filepath.Ext(url) 136 | fn = filepath.Join(dir, strings.TrimSuffix(filepath.Base(url), ext)+"-"+strconv.Itoa(i)+ext) 137 | } 138 | var err error 139 | out, err = os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 140 | if err != nil { 141 | if errors.Is(err, os.ErrExist) { 142 | out = nil 143 | continue 144 | } 145 | return fmt.Errorf("create file: %w", err) 146 | } 147 | fmt.Printf("Created %s\n", fn) 148 | } 149 | if out == nil { 150 | return errors.New("filed to create file") 151 | } 152 | defer func() { _ = out.Close() }() 153 | _, err = io.Copy(out, resp.Body) 154 | if err != nil { 155 | return fmt.Errorf("copy: %w", err) 156 | } 157 | 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /tasks/04/jsoncensor/censor_test.go: -------------------------------------------------------------------------------- 1 | package jsoncensor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCensorJSON(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | input []byte 14 | substr string 15 | expectedJSON string 16 | expectError bool 17 | }{ 18 | { 19 | name: "simple key-value pair", 20 | input: []byte(`{"k": "v", "key": "word_to_censor"}`), 21 | substr: "word_to_censor", 22 | expectedJSON: `{"k": "v", "key": "***"}`, 23 | expectError: false, 24 | }, 25 | { 26 | name: "nested JSON object", 27 | input: []byte(`{"k": "v", "another_key": {"arr": ["word_to_censor"], "inner_key": "what if i have a word_to_censor here???"}}`), 28 | substr: "word_to_censor", 29 | expectedJSON: `{"k": "v", "another_key": {"arr": ["***"], "inner_key": "***"}}`, 30 | expectError: false, 31 | }, 32 | { 33 | name: "array of strings", 34 | input: []byte(`["word_to_censor", "safe_word", "another_word_to_censor"]`), 35 | substr: "word_to_censor", 36 | expectedJSON: `["***", "safe_word", "***"]`, 37 | expectError: false, 38 | }, 39 | { 40 | name: "nested arrays", 41 | input: []byte(`{"arr": [["word_to_censor", "safe_word"], ["another_word_to_censor"]]}`), 42 | substr: "word_to_censor", 43 | expectedJSON: `{"arr": [["***", "safe_word"], ["***"]]}`, 44 | expectError: false, 45 | }, 46 | { 47 | name: "no match", 48 | input: []byte(`{"k": "v", "key": "safe_value"}`), 49 | substr: "word_to_censor", 50 | expectedJSON: `{"k": "v", "key": "safe_value"}`, 51 | expectError: false, 52 | }, 53 | { 54 | name: "invalid JSON", 55 | input: []byte(`{"k": "v", "key": "word_to_censor"`), // Missing closing brace 56 | substr: "word_to_censor", 57 | expectedJSON: "", 58 | expectError: true, 59 | }, 60 | { 61 | name: "empty JSON", 62 | input: []byte(`{}`), 63 | substr: "word_to_censor", 64 | expectedJSON: `{}`, 65 | expectError: false, 66 | }, 67 | { 68 | name: "empty string", 69 | input: []byte(``), 70 | substr: "word_to_censor", 71 | expectedJSON: "", 72 | expectError: true, 73 | }, 74 | { 75 | name: "complex nested JSON", 76 | input: []byte(`{"k1": "v1", "k2": {"k3": "word_to_censor", "k4": [{"k5": "word_to_censor"}, {"k6": "safe_value"}]}, "k7": ["word_to_censor", "safe_word"]}`), 77 | substr: "word_to_censor", 78 | expectedJSON: `{"k1": "v1", "k2": {"k3": "***", "k4": [{"k5": "***"}, {"k6": "safe_value"}]}, "k7": ["***", "safe_word"]}`, 79 | expectError: false, 80 | }, 81 | { 82 | name: "deeply nested JSON", 83 | input: []byte(`{"k1": {"k2": {"k3": {"k4": "word_to_censor"}}}}`), 84 | substr: "word_to_censor", 85 | expectedJSON: `{"k1": {"k2": {"k3": {"k4": "***"}}}}`, 86 | expectError: false, 87 | }, 88 | { 89 | name: "mixed types in array", 90 | input: []byte(`{"arr": ["word_to_censor", 123, true, {"k": "word_to_censor"}]}`), 91 | substr: "word_to_censor", 92 | expectedJSON: `{"arr": ["***", 123, true, {"k": "***"}]}`, 93 | expectError: false, 94 | }, 95 | { 96 | name: "multiple occurrences in one string", 97 | input: []byte(`{"k": "word_to_censor and another word_to_censor"}`), 98 | substr: "word_to_censor", 99 | expectedJSON: `{"k": "***"}`, 100 | expectError: false, 101 | }, 102 | } 103 | 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | res, err := CensorJSON(tt.input, tt.substr) 107 | 108 | if tt.expectError { 109 | assert.Error(t, err, "Expected an error") 110 | return 111 | } 112 | 113 | require.NoError(t, err) 114 | assert.JSONEq(t, tt.expectedJSON, string(res)) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tasks/04/retries/api_test.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type MockAPI struct { 14 | values map[string]*Value 15 | epochs map[string]uint64 16 | simulateErrors sync.Map 17 | writeErrors sync.Map 18 | simulateSetFail bool 19 | } 20 | 21 | func NewMockAPI() *MockAPI { 22 | return &MockAPI{ 23 | values: make(map[string]*Value), 24 | epochs: make(map[string]uint64), 25 | } 26 | } 27 | 28 | func (m *MockAPI) Get(key string) (*Value, uint64, error) { 29 | err, ok := m.simulateErrors.Load(key) 30 | if ok { 31 | return nil, 0, err.(error) 32 | } 33 | 34 | val, ok := m.values[key] 35 | if !ok { 36 | return nil, 0, APIError{status: StatusNotFound} 37 | } 38 | return val, m.epochs[key], nil 39 | } 40 | 41 | func (m *MockAPI) Set(key string, targetEpoch uint64, value *Value) error { 42 | err, ok := m.writeErrors.Load(key) 43 | if ok { 44 | return err.(error) 45 | } 46 | if m.simulateSetFail { 47 | return APIError{status: StatusFatalError} 48 | } 49 | if targetEpoch <= m.epochs[key] { 50 | return APIError{status: StatusValueTooOld} 51 | } 52 | m.values[key] = value 53 | m.epochs[key] = targetEpoch 54 | return nil 55 | } 56 | 57 | func TestUpdateValue_Success(t *testing.T) { 58 | api := NewMockAPI() 59 | api.values["key"] = &Value{} 60 | api.epochs["key"] = 1 61 | 62 | updateFn := func(currentValue *Value) (*Value, error) { 63 | return &Value{}, nil 64 | } 65 | 66 | err := UpdateValue(api, "key", updateFn) 67 | require.NoError(t, err, "Update should succeed") 68 | assert.Equal(t, uint64(2), api.epochs["key"], "Epoch should be incremented") 69 | } 70 | 71 | func TestUpdateValue_TransientErrors(t *testing.T) { 72 | api := NewMockAPI() 73 | api.simulateErrors.Store("key", ErrNetworkFault) 74 | api.values["key"] = &Value{} 75 | 76 | updateFn := func(currentValue *Value) (*Value, error) { 77 | return &Value{}, nil 78 | } 79 | 80 | // Simulate a successful update after a few retries 81 | go func() { 82 | time.Sleep(100 * time.Millisecond) 83 | api.simulateErrors.Delete("key") 84 | }() 85 | 86 | err := UpdateValue(api, "key", updateFn) 87 | require.NoError(t, err, "Update should succeed after retries") 88 | } 89 | 90 | func TestUpdateValue_ValueTooOld(t *testing.T) { 91 | api := NewMockAPI() 92 | api.writeErrors.Store("key", APIError{status: StatusValueTooOld}) 93 | api.values["key"] = &Value{} 94 | api.epochs["key"] = 1 95 | 96 | // Simulate a concurrent update 97 | go func() { 98 | time.Sleep(100 * time.Millisecond) 99 | api.writeErrors.Delete("key") 100 | }() 101 | 102 | updateFn := func(currentValue *Value) (*Value, error) { 103 | return &Value{}, nil 104 | } 105 | 106 | err := UpdateValue(api, "key", updateFn) 107 | require.NoError(t, err) 108 | assert.Equal(t, uint64(2), api.epochs["key"]) 109 | } 110 | 111 | func TestUpdateValue_KeyNotFound(t *testing.T) { 112 | api := NewMockAPI() 113 | 114 | updateFn := func(currentValue *Value) (*Value, error) { 115 | return &Value{}, nil 116 | } 117 | 118 | err := UpdateValue(api, "nonexistent", updateFn) 119 | require.Error(t, err) 120 | assert.ErrorAs(t, err, &APIError{}) 121 | assert.ErrorIs(t, err, APIError{status: StatusNotFound}) 122 | } 123 | 124 | func TestUpdateValue_FatalError(t *testing.T) { 125 | api := NewMockAPI() 126 | api.values["key"] = &Value{} 127 | api.epochs["key"] = 1 128 | api.simulateSetFail = true 129 | 130 | updateFn := func(currentValue *Value) (*Value, error) { 131 | return &Value{}, nil 132 | } 133 | 134 | err := UpdateValue(api, "key", updateFn) 135 | require.Error(t, err) 136 | assert.ErrorAs(t, err, &APIError{}) 137 | assert.ErrorIs(t, err, APIError{status: StatusFatalError}) 138 | } 139 | 140 | func TestUpdateValue_ValueUpdaterFailure(t *testing.T) { 141 | api := NewMockAPI() 142 | api.values["key"] = &Value{} 143 | api.epochs["key"] = 1 144 | 145 | updateFn := func(currentValue *Value) (*Value, error) { 146 | return nil, errors.New("update failed") 147 | } 148 | 149 | err := UpdateValue(api, "key", updateFn) 150 | require.Error(t, err) 151 | } 152 | -------------------------------------------------------------------------------- /lectures/01/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 1. Введение 4 | 5 | ## Полезные ссылки 6 | 7 | * [Официальный сайт](https://golang.org/) 8 | * [The Go Playground](https://play.golang.org/) 9 | * [The Go Programming Language](https://www.gopl.io/) (есть на русском языке) 10 | 11 | ## Отчетность 12 | 13 | * Небольшие домашние задания после семинаров (проверяются автоматикой) 14 | * Вероятно, один или несколько больших проектов (проверяются вручную) 15 | * Коммуникация - через телеграм-чат 16 | 17 | ## Язык Go 18 | 19 | **Go** (часто также **Golang**) — компилируемый многопоточный язык программирования, разработанный внутри компании Google. 20 | 21 | Официально язык был представлен в ноябре 2009 года. 22 | 23 | Разрабатывался как язык программирования для создания высокоэффективных программ, 24 | работающих на **современных распределённых системах** и **многоядерных процессорах**. 25 | 26 | ## Язык Go 27 | 28 | Среди недостатков других языков, с которыми боролись создатели Go, можно выделить: 29 | * Медленная сборка программ 30 | * Неконтролируемые зависимости 31 | * Сложность языковых конструкций, множество способов решить задачу 32 | * Отсутствие поддержки конкурентных программ на ровне языка, следствие - неудобство разработки 33 | * Затруднения с пониманием программ, вызванные неудобочитаемостью кода, плохим документированием и так далее 34 | * Сложность разработки инструментария 35 | 36 | ## Язык Go 37 | 38 | Основными требованиями к языку стали: 39 | * Ортогональность 40 | * Простая и регулярная грамматика 41 | * Простая работа с типами 42 | * Отсутствие неявных преобразований 43 | * Сборка мусора 44 | * Встроенные средства распараллеливания, простые и эффективные 45 | * Поддержка строк (unicode), ассоциативных массивов и коммуникационных каналов 46 | * Чёткое разделение интерфейса и реализации 47 | * Эффективная система пакетов с явным указанием зависимостей, обеспечивающая быструю сборку 48 | 49 | ## Язык Go 50 | 51 | Возможности языка: 52 | * Строгая статическая типизация 53 | * Полноценная поддержка указателей, но без арифметики 54 | * Строковый тип с поддержкой unicode 55 | * Средства функционального программирования 56 | * Автоматическое управление памятью со сборщиком мусора 57 | * Средства параллельного и конкурентного программирования 58 | 59 | 60 | ## Язык Go 61 | 62 | Отличия Go от других популярных языков: 63 | * Отсутствие исключений, явная обработка ошибок (в "стиле C") 64 | * Отсутствие наследования (вместо - вложение типов) 65 | * Отсутствие дженериков (но совсем скоро будут) 66 | * Отсутствует возможность переопределять методы и функции 67 | * Ряд ограничений, приводяций к усложнению или ошибкам: некоторые операции над массивами, поддержка отрицательных индексов, принцип "любое выражение возвращает значение" 68 | 69 | ## Hello, world 70 | 71 | .code code/hello/hello.go 72 | 73 | ## Echo 74 | 75 | .code code/echo/echo.go 76 | 77 | ## For loop 78 | 79 | for initialization; condition; post { 80 | // statements 81 | } 82 | 83 | for condition { 84 | // statements 85 | } 86 | 87 | 88 | for { 89 | // statements 90 | } 91 | 92 | 93 | ## Echo2 94 | 95 | .code code/echo2/echo.go 96 | 97 | ## Variables 98 | 99 | s := "" 100 | var s string 101 | var s = "" 102 | var s string = "" 103 | 104 | Последние два варианта используются редко, в основном - первые два 105 | 106 | ## Echo3 107 | 108 | .code code/echo3/echo.go 109 | 110 | ## Dup 111 | 112 | .code code/dup/main.go 113 | 114 | ## Formatting 115 | 116 | Функции форматирования заканчиваются на `f`. `fmt.Errorf`, `log.Printf`. 117 | 118 | %d decimal integer 119 | %x, %o, %b integer in hexade cimal, octal, binary 120 | %f, %g, %e floating-point number: 3.141593 3.141592653589793 3.141593e+00 121 | %t boolean: true or false 122 | %c rune (Unicode code point) 123 | %s string 124 | %q quoted string "abc" or rune 'c' 125 | %v any value in a natural format 126 | %T type of any value 127 | %% literal percent sign (no operand) 128 | 129 | ## Dup2 130 | 131 | .code code/dup2/main.go /^func main/,/^}/ 132 | 133 | ## Dup3 134 | 135 | .code code/dup3/main.go /^func main/,/^}/ 136 | 137 | ## Fetch 138 | 139 | .code code/fetch/main.go 140 | 141 | ## FetchAll 142 | 143 | .code code/fetchall/main.go /^func main/,/^}/ 144 | 145 | ## FetchAll 146 | 147 | .code code/fetchall/main.go /^func fetch/,/^}/ 148 | -------------------------------------------------------------------------------- /lectures/08/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 8. Reflection 4 | 5 | ## Reflection 6 | 7 | * Рефлексия — способность программы исследовать собственную структуру, в особенности через типы. 8 | Это форма метапрограммирования и отличный источник путаницы. (c) [Законы рефлексии в Gо](https://habr.com/ru/post/415171/) 9 | * Пригождается, когда необходимо реализовать обобщенную функцию, про аргументы которой мы ничего не знаем 10 | 11 | ## Fprintf 12 | 13 | * Типичный пример - `fmt.Fprintf`. Эта функция умеет форматировать почти любые входные данные (в том 14 | числе типы, объявленные пользователем) 15 | * Для вывода значений мы можем использовать type switch, однако мы будем ограничены конечным набором типов 16 | (а еще надо уметь форматировать слайсы и словари любых типов и любой вложенности) 17 | 18 | type stringer interface { 19 | String() string 20 | } 21 | switch x := x.(type) { 22 | case stringer: 23 | return x.String() 24 | case string: 25 | return x 26 | case int: 27 | return strconv.Itoa(x) 28 | // ...similar cases for int16, uint32, and so on... 29 | 30 | * Для реализации задуманного нам надо уметь заглядывать в структуру неизвестных заранее нам типов 31 | 32 | ## reflect.Type 33 | 34 | * Один из двух основных типов пакета `reflect` 35 | * `Type` представляет некоторый тип в Go и позволяет узнавать многое о том, как этот тип устроен 36 | (какие есть поля, аргументы функций, ...) 37 | * Функция `TypeOf` принимает `interface{}` и возвращает `Type` для переданного аргумента 38 | 39 | t := reflect.TypeOf(3) // a reflect.Type 40 | fmt.Println(t.String()) // "int" 41 | fmt.Println(t) // "int" 42 | 43 | * Стоит учитывать, что `TypeOf` всегда возвращает динамический тип (т.е. в случае передачи интерфейса будет 44 | возвращена информация о конкретном типе, не о интерфейсе) 45 | 46 | var w io.Writer = os.Stdout 47 | fmt.Println(reflect.TypeOf(w)) // "*os.File" 48 | 49 | * Для дебага бывает удобно использовать `%T` в `Printf`, который внутри использует `reflect.TypeOf` 50 | 51 | ## reflect.Value 52 | 53 | * Второй основной тип пакета `reflect` 54 | * `Value` может хранить любое значение любого типа 55 | * `ValueOf` принимает `interface{}` и возвращает `Value`, содержащий значение 56 | * Результат `ValueOf` всегда конкретный, но может также содержать значение интерфейса 57 | 58 | v := reflect.ValueOf(3) // a reflect.Value 59 | fmt.Println(v) // "3" 60 | fmt.Printf("%v\n", v) // "3" 61 | fmt.Println(v.String()) // NOTE: "" 62 | 63 | * По `Value` получить обратно `interface{}` со значением можно с помощью метода `reflect.Value.Interface` 64 | * Разница между `interface{}` и `reflect.Value` заключается в том, что без знания конкретного типа 65 | мы не можем толком ничего сделать с `interface{}` (т.к. мы можем использовать только type assertion). `reflect.Value` 66 | же позволяет изучать содержание, ничего не зная о типе 67 | 68 | ## reflect.Value 69 | 70 | * Метод `Kind` позволяет получить значение перечисления, чем является данное значение 71 | * Для массивов и слайсов (и для строк): `Len` и `Index` 72 | * Для структур: `NumField` и `Field` (включая встроенные и сокрытые) 73 | * Для словарей: `MapKeys` и `MapIndex` 74 | * Для указателей: `IsNil` и `Elem` 75 | * Для интерфейсов: `IsNil` и `Elem` 76 | 77 | ## reflect.Value 78 | 79 | * Кроме доступа до данных, `Value` позволяет также изменять их 80 | * Однако, изменять можно только значения с адресом (можно проверить через `CanAddr`) 81 | * Кроме этого, нельзя изменять неэкспортированные поля структур 82 | * В общем случае, проверить, можно ли менять значение можно через `CanSet` 83 | 84 | ## Тэги 85 | 86 | * Ранее мы использовали тэги при работе с `encoding/json`. Данный механизм применяется в большом количестве 87 | пакетов и позволяет указывать дополнительную инофрмацию как надо обрабатывать то или иное поле структуры 88 | * Получить тэг для поля можно через поле `Tag` у `reflect.Type` 89 | * Далее получить интересующее нас значение можно через `Get` 90 | 91 | ## Reflection 92 | 93 | * В ситуациях, где это возможно, старайтесь избегать использование reflection 94 | * Использование reflections сильно замедляет код (см., например, [easyjson](https://github.com/mailru/easyjson)) 95 | * Очень легко допустить ошибку (например, не предусмотреть возможные входные параметры) при написании кода с reflection 96 | * Нужно максимально проверять возможность операций перед их выполнением (иначе вылетит паника) 97 | * Кроме этого, использование reflection сильно затрудняет возможности автоматическим анализаторам и программам 98 | для рефакторинга -------------------------------------------------------------------------------- /tasks/03/ini/parse_test.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/dbeliakov/mipt-golang-course/utils/testutils" 11 | ) 12 | 13 | func TestParse_ErrorOnNonExistentFile(t *testing.T) { 14 | // NOTE: file with such name cannot exist, because file name is too long 15 | nonExistentFile := strings.Repeat("abcd", 512) 16 | 17 | _, err := Parse(nonExistentFile) 18 | require.Error(t, err, "Parser should return an error when file does not exist") 19 | } 20 | 21 | func TestParse_CommonCases(t *testing.T) { 22 | testCases := []struct { 23 | name string 24 | content string 25 | expected map[string]map[string]string 26 | invalidFormat bool 27 | }{ 28 | { 29 | name: "empty", 30 | content: "", 31 | expected: map[string]map[string]string{}, 32 | }, 33 | { 34 | name: "single header without keys", 35 | content: "[header1]", 36 | expected: map[string]map[string]string{}, 37 | }, 38 | { 39 | name: "single header with keys", 40 | content: ` 41 | [some_header] 42 | key1 = value 43 | k2= val2 44 | `, 45 | expected: map[string]map[string]string{ 46 | "some_header": { 47 | "key1": "value", 48 | "k2": "val2", 49 | }, 50 | }, 51 | }, 52 | { 53 | name: "empty lines inside header", 54 | content: ` 55 | [header] 56 | k1 = v1 57 | 58 | 59 | 60 | 61 | k2 = v2 62 | `, 63 | expected: map[string]map[string]string{ 64 | "header": { 65 | "k1": "v1", 66 | "k2": "v2", 67 | }, 68 | }, 69 | }, 70 | { 71 | name: "multiple headers without keys", 72 | content: ` 73 | [header1] 74 | [header2] 75 | [header3] 76 | `, 77 | expected: map[string]map[string]string{}, 78 | }, 79 | { 80 | name: "multiple headers with keys", 81 | content: ` 82 | [1] 83 | key = first_value 84 | 85 | [second] 86 | key = 2_value 87 | 88 | [third] 89 | key = third_value 90 | 91 | [4] 92 | key = 4 93 | `, 94 | expected: map[string]map[string]string{ 95 | "1": { 96 | "key": "first_value", 97 | }, 98 | "second": { 99 | "key": "2_value", 100 | }, 101 | "third": { 102 | "key": "third_value", 103 | }, 104 | "4": { 105 | "key": "4", 106 | }, 107 | }, 108 | }, 109 | { 110 | name: "duplicate keys are overwritten in single header", 111 | content: ` 112 | [header] 113 | duplicate_key = first_value 114 | unique_key = unique_value 115 | duplicate_key = last_value 116 | `, 117 | expected: map[string]map[string]string{ 118 | "header": { 119 | "duplicate_key": "last_value", 120 | "unique_key": "unique_value", 121 | }, 122 | }, 123 | }, 124 | { 125 | name: "duplicate headers are merged together", 126 | content: ` 127 | [duplicate_header] 128 | key = value 129 | 130 | [unique_header] 131 | unique_key = some_unique_value 132 | 133 | [duplicate_header] 134 | another_key = another_value 135 | `, 136 | expected: map[string]map[string]string{ 137 | "duplicate_header": { 138 | "key": "value", 139 | "another_key": "another_value", 140 | }, 141 | "unique_header": { 142 | "unique_key": "some_unique_value", 143 | }, 144 | }, 145 | }, 146 | { 147 | name: "duplicate keys are overwritten in duplicate header when merged", 148 | content: ` 149 | [header] 150 | k = 1 151 | 152 | [header] 153 | k = 2 154 | 155 | [another_header] 156 | k = 3 157 | 158 | [header] 159 | k = 4 160 | `, 161 | expected: map[string]map[string]string{ 162 | "header": { 163 | "k": "4", 164 | }, 165 | "another_header": { 166 | "k": "3", 167 | }, 168 | }, 169 | }, 170 | { 171 | name: "error on empty header inside brackets", 172 | content: ` 173 | [] 174 | value = 10 175 | `, 176 | invalidFormat: true, 177 | }, 178 | { 179 | name: "error on multiple equal signs inside key-value definition", 180 | content: ` 181 | [header] 182 | a=b=c 183 | `, 184 | invalidFormat: true, 185 | }, 186 | { 187 | name: "error on key-value pair outside headers", 188 | content: ` 189 | key = value 190 | 191 | [header] 192 | another_key = value 193 | `, 194 | invalidFormat: true, 195 | }, 196 | { 197 | name: "error on key without a value", 198 | content: ` 199 | [header] 200 | key 201 | `, 202 | invalidFormat: true, 203 | }, 204 | } 205 | 206 | for _, tc := range testCases { 207 | t.Run(tc.name, func(t *testing.T) { 208 | fileName := testutils.TempFile(t, tc.content) 209 | actual, err := Parse(fileName) 210 | if tc.invalidFormat { 211 | require.Error(t, err) 212 | assert.ErrorIs(t, err, ErrFileIsMalformed) 213 | } else { 214 | require.NoError(t, err) 215 | assert.Equal(t, tc.expected, actual) 216 | } 217 | }) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tasks/05/downloader/downloader_test.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type mockTransport struct { 15 | responses map[string]*http.Response 16 | errors map[string]error 17 | } 18 | 19 | func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 20 | if err, ok := m.errors[req.URL.String()]; ok { 21 | return nil, err 22 | } 23 | if resp, ok := m.responses[req.URL.String()]; ok { 24 | return resp, nil 25 | } 26 | return nil, errors.New("unexpected URL") 27 | } 28 | 29 | func newMockClient(responses map[string]*http.Response, errors map[string]error) *http.Client { 30 | return &http.Client{ 31 | Transport: &mockTransport{ 32 | responses: responses, 33 | errors: errors, 34 | }, 35 | } 36 | } 37 | 38 | func TestDownloader_Download_Success(t *testing.T) { 39 | responses := map[string]*http.Response{ 40 | "http://success.com": { 41 | StatusCode: http.StatusOK, 42 | Body: io.NopCloser(bytes.NewBufferString("test content")), 43 | }, 44 | } 45 | client := newMockClient(responses, nil) 46 | downloader := &Downloader{client: client} 47 | 48 | results, errs := downloader.Download("http://success.com") 49 | 50 | require.Empty(t, errs) 51 | require.Len(t, results, 1) 52 | assert.Equal(t, []byte("test content"), results["http://success.com"]) 53 | } 54 | 55 | func TestDownloader_Download_NetworkError(t *testing.T) { 56 | mockErr := errors.New("connection failed") 57 | errors := map[string]error{ 58 | "http://error.com": mockErr, 59 | } 60 | client := newMockClient(nil, errors) 61 | downloader := &Downloader{client: client} 62 | 63 | results, errs := downloader.Download("http://error.com") 64 | 65 | require.Empty(t, results) 66 | require.Len(t, errs, 1) 67 | assert.IsType(t, &DownloadError{}, errs[0]) 68 | assert.Equal(t, "http://error.com", errs[0].(*DownloadError).URL()) 69 | assert.Contains(t, errs[0].Error(), "connection failed") 70 | } 71 | 72 | func TestDownloader_Download_ReadError(t *testing.T) { 73 | responses := map[string]*http.Response{ 74 | "http://badbody.com": { 75 | StatusCode: http.StatusOK, 76 | Body: io.NopCloser(&failingReader{}), 77 | }, 78 | } 79 | client := newMockClient(responses, nil) 80 | downloader := &Downloader{client: client} 81 | 82 | results, errs := downloader.Download("http://badbody.com") 83 | 84 | require.Empty(t, results) 85 | require.Len(t, errs, 1) 86 | assert.IsType(t, &DownloadError{}, errs[0]) 87 | assert.Equal(t, "http://badbody.com", errs[0].(*DownloadError).URL()) 88 | assert.Contains(t, errs[0].Error(), "read error") 89 | } 90 | 91 | func TestDownloader_Download_MixedResults(t *testing.T) { 92 | responses := map[string]*http.Response{ 93 | "http://good.com": { 94 | StatusCode: http.StatusOK, 95 | Body: io.NopCloser(bytes.NewBufferString("good content")), 96 | }, 97 | } 98 | errors := map[string]error{ 99 | "http://bad.com": errors.New("failed"), 100 | } 101 | client := newMockClient(responses, errors) 102 | downloader := &Downloader{client: client} 103 | 104 | results, errs := downloader.Download("http://good.com", "http://bad.com") 105 | 106 | require.Len(t, results, 1) 107 | assert.Equal(t, []byte("good content"), results["http://good.com"]) 108 | require.Len(t, errs, 1) 109 | assert.IsType(t, &DownloadError{}, errs[0]) 110 | assert.Equal(t, "http://bad.com", errs[0].(*DownloadError).URL()) 111 | } 112 | 113 | func TestDownloader_Download_EmptyURLs(t *testing.T) { 114 | client := newMockClient(nil, nil) 115 | downloader := &Downloader{client: client} 116 | 117 | results, errs := downloader.Download() 118 | 119 | assert.Empty(t, results) 120 | assert.Empty(t, errs) 121 | } 122 | 123 | func TestDownloader_Download_NonOKStatusCode(t *testing.T) { 124 | responses := map[string]*http.Response{ 125 | "http://notfound.com": { 126 | StatusCode: http.StatusNotFound, 127 | Body: io.NopCloser(bytes.NewBufferString("not found")), 128 | }, 129 | } 130 | client := newMockClient(responses, nil) 131 | downloader := &Downloader{client: client} 132 | 133 | results, errs := downloader.Download("http://notfound.com") 134 | 135 | assert.Empty(t, errs) 136 | require.Contains(t, results, "http://notfound.com") 137 | assert.Equal(t, results["http://notfound.com"], []byte("not found")) 138 | } 139 | 140 | type failingReader struct{} 141 | 142 | func (f *failingReader) Read(p []byte) (n int, err error) { 143 | return 0, errors.New("read error") 144 | } 145 | 146 | func TestDownloadError(t *testing.T) { 147 | mockErr := errors.New("mock error") 148 | de := &DownloadError{ 149 | url: "http://test.com", 150 | err: mockErr, 151 | } 152 | 153 | assert.Equal(t, "http://test.com", de.URL()) 154 | assert.Equal(t, `download "http://test.com": mock error`, de.Error()) 155 | assert.Equal(t, mockErr, de.Unwrap()) 156 | } 157 | -------------------------------------------------------------------------------- /lectures/06/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 6. Механизмы синхронизации 4 | 5 | ## Мультиплексирование 6 | 7 | * Мультиплексирование позволяет объединять чтение (или запись) для нескольких каналов 8 | * Из-за удобства этого механизма некоторые сущности, не связанные с коммуникацией, используют каналы 9 | (`context.Done`, `time.After`) 10 | * Кроме этого, данный механизмм позволяет прочитать или записать что-то без блокировки (т.е. в случае, если бы 11 | случилась блокировка, мы выйдем, ничего не записав и не прочитав) 12 | 13 | select { 14 | case <-ch: 15 | // ... 16 | case <-ch2: 17 | // ... 18 | default: 19 | // ... 20 | } 21 | 22 | ## Отмена исполнения горутин 23 | 24 | * Есть несколько способов (фактически, один, но разными сущностями) правильно отменить исполнение горутины 25 | * Один - передать канал, из которого в select дожидаться сообщения о том, что можно завершиться 26 | * Недостаток - сложно отменить некоторые потенциально долгие вещи (такие, как http-запрос) 27 | * Другой способ - передавать контекст (пакет `context`) 28 | * Две ключевых роли контекста - передача сигнала об отмене и передача данных далее при обработке (например, пользовательского запроса) 29 | 30 | ## Race condition 31 | 32 | * При выполнении программы конкурентно мы не можем утверждать, что событие в одной горутине 33 | произошло раньше или позже, чем другое событие в другой горутине 34 | * Функция (или метод) concurrent-safe в случае, когда ее можно использовать из разных 35 | горутин без дополнительной синхронизации. Тип concurrent-safe, если все его методы concurrent-safe 36 | * В большинстве языков программирования (включая Go) большинство типов не concurrent-safe, 37 | так как накладывает дополнительные расходы, которые далеко не всегда нужны 38 | * В то же время, большинство функций в пакетах считаются concurrent-safe (но если они меняют 39 | глобальные объекты, то могут быть и не concurrent-safe) 40 | 41 | ## Race condition 42 | 43 | * Состояние гонки - ошибка проектирования многопоточной системы или приложения, при которой работа системы 44 | или приложения зависит от того, в каком порядке выполняются части кода 45 | * Зачастую, состояние гонки возникает из-за отсутствия атомарности некоторых действий (т.е. другая 46 | горутина может увидеть промежуточный результат в моменте выполнения операции) 47 | * Даже большинство операций с числами не атомарны 48 | * В большинстве ситуаций, когда некоторые данные меняются из нескольких горутин, возникает 49 | data race 50 | 51 | ## Data race 52 | 53 | * Нужно понимать, что data race может возникать только когда хотя бы одна горутина из нескольких 54 | меняет данные 55 | * Вполне корректно, например, инициализировать map в одном месте и потом **читать** из него 56 | из нескольких горутин 57 | * Но как только появляется горутина, которая пытается **писать** в map, то сразу же возникает 58 | data race 59 | * Каналы позволяют избегать data race путем коммуникации через общий объект, но данные передаются 60 | в виде копии (и не возникает конкурентного доступа к общей памяти) 61 | 62 | ## sync.Mutex 63 | 64 | * Мьютекс позволяет "ограничить" критические секции кода, где могут возникать гонки 65 | * Когда мьютекс захвачен (`.Lock()` выполнился), гарантируется, что данная горутина единственная, 66 | кто сейчас имеет заблокированный мьютекс 67 | * Таким образом, мы можем "приостановить" исполнение других горутин, оперирующих с теми же 68 | данными до момента, пока мы не закончим свои действия 69 | * Обязательно необходимо отпустить мьютекс после выполнения операции (`.Unlock()`) 70 | * При этом страдает "параллелилизм" программы - появляются точки синхронизации 71 | * Под мьютекс следует заносить только критические секции, и стараться разблокировать его как можно 72 | быстрее 73 | 74 | ## sync.RWMutex 75 | 76 | * Продвинутый мьютекс, который позволяет конкурентно читать какие-либо данные и эксклюзивно их модифицировать 77 | * Полезен в ситуациях, когда данные редко обновляются, но часто используются (например, раз в минуту 78 | приходит новый курс доллара, а в течении этой минуты происходит множество конкурентных операций, использующих 79 | это значение) 80 | * Для использования читателями появляются методы `.RLock()` и `.RUnlock()` 81 | 82 | ## sync.Once 83 | 84 | * Позволяет выполнить некоторую операцию ровно один раз (в одной из горутин, остальные дождутся 85 | выполнения) 86 | * Полезен, когда необходимо произвести некоторую инициализацию в момент первого вызова 87 | и в дальнейшем использовать эти данные 88 | * Объект имеет только один метод `.Do`, принимающий функцию для выполнения 89 | 90 | 91 | ## Race detector 92 | 93 | * Если запустить `go test`, `go build` или `go run` с флагом `-race`, будет собрана специальная 94 | версия программы, которая будет медленнее исполняться (так что это не для продакшена), но будет 95 | отслеживать обращения к общей памяти 96 | * Кроме этого, в этой версии программы будут отслеживаться все события синхронизации 97 | * В случае, если находятся обращения к общей памяти без синхронизации, то программа сообщает об этом 98 | * Встроенный race detector позволяет разработчикам сэкономить часы и даже дни поиска ошибок 99 | * Подробнее можно почитать в `The Go Memory Model` 100 | 101 | -------------------------------------------------------------------------------- /tasks/05/datatransfer/transfer_test.go: -------------------------------------------------------------------------------- 1 | package datatransfer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type TestProducer struct { 15 | items []any 16 | pos int 17 | mu sync.Mutex 18 | readDelay time.Duration 19 | failAfter int 20 | failWith error 21 | } 22 | 23 | func NewTestProducer(items ...any) *TestProducer { 24 | return &TestProducer{items: items} 25 | } 26 | 27 | func (p *TestProducer) ReadNext() (any, error) { 28 | if p.readDelay > 0 { 29 | time.Sleep(p.readDelay) 30 | } 31 | 32 | p.mu.Lock() 33 | defer p.mu.Unlock() 34 | 35 | if p.failAfter > 0 { 36 | p.failAfter-- 37 | if p.failAfter == 0 { 38 | return nil, p.failWith 39 | } 40 | } 41 | 42 | if p.pos >= len(p.items) { 43 | return nil, ErrProducerFinished 44 | } 45 | 46 | item := p.items[p.pos] 47 | p.pos++ 48 | return item, nil 49 | } 50 | 51 | func (p *TestProducer) SetReadDelay(delay time.Duration) { 52 | p.readDelay = delay 53 | } 54 | 55 | func (p *TestProducer) SetFailAfter(n int, err error) { 56 | p.failAfter = n 57 | p.failWith = err 58 | } 59 | 60 | type TestConsumer struct { 61 | chunks [][]any 62 | mu sync.Mutex 63 | writeDelay time.Duration 64 | failAfter int 65 | failWith error 66 | } 67 | 68 | func NewTestConsumer() *TestConsumer { 69 | return &TestConsumer{} 70 | } 71 | 72 | func (c *TestConsumer) WriteMany(items []any) error { 73 | if c.writeDelay > 0 { 74 | time.Sleep(c.writeDelay) 75 | } 76 | 77 | c.mu.Lock() 78 | defer c.mu.Unlock() 79 | 80 | if c.failAfter > 0 { 81 | c.failAfter-- 82 | if c.failAfter == 0 { 83 | return c.failWith 84 | } 85 | } 86 | 87 | chunk := make([]any, len(items)) 88 | copy(chunk, items) 89 | c.chunks = append(c.chunks, chunk) 90 | return nil 91 | } 92 | 93 | func (c *TestConsumer) Chunks() [][]any { 94 | c.mu.Lock() 95 | defer c.mu.Unlock() 96 | return c.chunks 97 | } 98 | 99 | func (c *TestConsumer) Data() []any { 100 | c.mu.Lock() 101 | defer c.mu.Unlock() 102 | 103 | var data []any 104 | for _, c := range c.chunks { 105 | data = append(data, c...) 106 | } 107 | 108 | return data 109 | } 110 | 111 | func (c *TestConsumer) SetWriteDelay(delay time.Duration) { 112 | c.writeDelay = delay 113 | } 114 | 115 | func (c *TestConsumer) SetFailAfter(n int, err error) { 116 | c.failAfter = n 117 | c.failWith = err 118 | } 119 | 120 | func TestTransferData_SuccessfulTransfer(t *testing.T) { 121 | t.Parallel() 122 | producer := NewTestProducer(1, 2, 3, 4, 5) 123 | consumer := NewTestConsumer() 124 | 125 | transfer := NewTransfer(2, 3) 126 | err := transfer.TransferData(context.Background(), producer, consumer) 127 | 128 | require.NoError(t, err) 129 | assert.ElementsMatch(t, []any{ 130 | 1, 2, 3, 4, 5, 131 | }, consumer.Data()) 132 | } 133 | 134 | func TestTransferData_EmptyProducer(t *testing.T) { 135 | t.Parallel() 136 | producer := NewTestProducer() 137 | consumer := NewTestConsumer() 138 | 139 | transfer := NewTransfer(2, 3) 140 | err := transfer.TransferData(context.Background(), producer, consumer) 141 | 142 | require.NoError(t, err) 143 | assert.Empty(t, consumer.Chunks()) 144 | } 145 | 146 | func TestTransferData_ProducerError(t *testing.T) { 147 | t.Parallel() 148 | producerErr := errors.New("producer error") 149 | producer := NewTestProducer(1, 2, 3) 150 | producer.SetFailAfter(2, producerErr) 151 | 152 | consumer := NewTestConsumer() 153 | 154 | transfer := NewTransfer(2, 3) 155 | err := transfer.TransferData(context.Background(), producer, consumer) 156 | 157 | require.Error(t, err) 158 | assert.ErrorIs(t, err, producerErr) 159 | assert.Len(t, consumer.Chunks(), 1) 160 | } 161 | 162 | func TestTransferData_ConsumerError(t *testing.T) { 163 | t.Parallel() 164 | consumerErr := errors.New("consumer error") 165 | producer := NewTestProducer(1, 2) 166 | consumer := NewTestConsumer() 167 | consumer.SetFailAfter(1, consumerErr) 168 | 169 | transfer := NewTransfer(1, 3) 170 | err := transfer.TransferData(context.Background(), producer, consumer) 171 | 172 | require.Error(t, err) 173 | assert.ErrorIs(t, err, consumerErr) 174 | assert.Empty(t, consumer.Chunks()) 175 | } 176 | 177 | func TestTransferData_ContextCancel(t *testing.T) { 178 | t.Parallel() 179 | producer := NewTestProducer(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 180 | producer.SetReadDelay(50 * time.Millisecond) 181 | 182 | consumer := NewTestConsumer() 183 | 184 | ctx, cancel := context.WithCancel(context.Background()) 185 | defer cancel() 186 | 187 | go func() { 188 | time.Sleep(10 * time.Millisecond) 189 | cancel() 190 | }() 191 | 192 | transfer := NewTransfer(2, 3) 193 | err := transfer.TransferData(ctx, producer, consumer) 194 | 195 | require.Error(t, err) 196 | assert.ErrorIs(t, err, context.Canceled) 197 | } 198 | 199 | func TestTransferData_ConcurrentWriters(t *testing.T) { 200 | t.Parallel() 201 | items := make([]any, 100) 202 | for i := 0; i < 100; i++ { 203 | items[i] = i 204 | } 205 | 206 | producer := NewTestProducer(items...) 207 | consumer := NewTestConsumer() 208 | 209 | transfer := NewTransfer(4, 10) 210 | err := transfer.TransferData(context.Background(), producer, consumer) 211 | 212 | require.NoError(t, err) 213 | 214 | total := len(consumer.Data()) 215 | assert.Equal(t, 100, total) 216 | } 217 | 218 | func TestTransferData_ProducerFinished(t *testing.T) { 219 | t.Parallel() 220 | producer := NewTestProducer(1, 2) 221 | consumer := NewTestConsumer() 222 | 223 | transfer := NewTransfer(1, 1) 224 | err := transfer.TransferData(context.Background(), producer, consumer) 225 | 226 | require.NoError(t, err) 227 | assert.Equal(t, [][]any{{1}, {2}}, consumer.Chunks()) 228 | } 229 | 230 | func TestTransferData_LargeChunkSize(t *testing.T) { 231 | t.Parallel() 232 | producer := NewTestProducer(1, 2, 3, 4, 5) 233 | consumer := NewTestConsumer() 234 | 235 | transfer := NewTransfer(2, 10) 236 | err := transfer.TransferData(context.Background(), producer, consumer) 237 | 238 | require.NoError(t, err) 239 | assert.ElementsMatch(t, []any{1, 2, 3, 4, 5}, consumer.Data()) 240 | } 241 | -------------------------------------------------------------------------------- /lectures/07/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 7. Тестирование 4 | 5 | ## Тестирование 6 | 7 | * С увеличением кодовой базы проекта (и количества фичей) все сложнее тестировать код 8 | "руками" 9 | * Чем сложнее система, тем больше вероятность что-то сломать при внесении изменений в код 10 | * Современные парадигмы разработки продуктов зачастую предполагают большое количетсво 11 | "мелких" релизов, автоматически доезжающих до продакшена (с помощью CI/CD) 12 | * При таком подходе невозможно на каждое изменение протестировать весь функционал - 13 | на помощь приходят автоматизированные тесты (unit, функциональные, интеграционные, ...) 14 | * Основное назначение тестов (очень грубо) - проверить, что код работает так, как ожидается 15 | * В отличии от многих других языков программирования, инструменты для тестирования 16 | программ входят в стандартный тулчейн 17 | 18 | ## Тестирование в Go 19 | 20 | * Тесты в Go располагаются в файлах `*_test.go`. Эти файлы "не видны" при сборке программы и 21 | используются только при запуске тестов через `go test` 22 | * Кроме тестов, в этих файлах могут находиться бенчмарки и примеры кода для документации 23 | * Тест - обычная функция с именем `Test***`, принимающая в качестве аргумента специальный 24 | объект `t *testing.T` 25 | * Аналогично, бенчмарки носят имя `Benchmark***`; примеры - `Example***` 26 | * Момент вызова `go test` функции в `*_test.go` файлах сканируются и создается временный 27 | `main`-пакет, в котором вызываются нужные тесты 28 | 29 | ## Тестирование в Go 30 | 31 | * Функция для теста выглядит следующим образом: 32 | 33 | func TestName(t *testing.T) { 34 | // ... 35 | } 36 | 37 | * Суффикс (после префикса `Test`) должен начинаться с большой буквы 38 | * Имя теста должно пояснять, какую функциональность (или часть функциональности) проверяет этот тест 39 | * Аргумент функции необходим для сообщения о неуспешности проверок и логирования дополнительной информации 40 | * Сообщить об ошибке можно через `t.Errorf`, завершить тест - `t.Fatalf` 41 | * Есть некоторое количество пакетов, позволяющих не взаимодействовать с `t` напрямую 42 | 43 | ## Рандомизированное тестирование 44 | 45 | * Тесты с заранее известными параметрами и результатом (табличные тесты) - один из вариантов тестов, 46 | позволяющий протестировать важные и граничные кейсы работы 47 | * Другой подход - рандомизированные тесты, когда входные параметры генерируются случайным образом 48 | * В этом случае, результаты можно вычислять, например, более простым (и менее эффективным) 49 | алгоритмом, который гораздо проще в имплементации 50 | * В случае рандомизированных тестов важно выводить в лог достаточно информации для воспроизведения 51 | прохождения тестов 52 | * К рандомизированным тестам можно отнести fuzzing - когда мы передаем функции 53 | неправильные, неожиданные или случайные данные. [Фаззинг: новое юнит тестирование. Дмитрий Вьюков, Google.](https://www.youtube.com/watch?v=EJVp13f_aIs) 54 | 55 | ## Тестирование в Go 56 | 57 | * Тестировать можно также и main пакеты - в этом случае, зачастую, основную логику работы 58 | выносят в отдельную функцию, а main занимается тривиальными операциями (например, парсит флаги) 59 | * В этом случае важно, чтобы тестируемые функции не вызывали функции, завершающие выполнение 60 | программы - например, `log.Fatal` или `os.Exit` 61 | 62 | ## Тестирование в Go 63 | 64 | * Тесты (условно) можно разделить на два вида: `White Box` и `Black Box` 65 | * В случае `Black Box` тестов проверяется только внешнее API (пакета, сервиса, ...) 66 | * В случае тестирования пакета можно тесты положить в отдельный пакет (с именем `*_test`), 67 | в этом случае тесты не будут иметь доступа до внутренностей пакета 68 | * В случае `White Box` тесты пишутся с оглядкой на внутренности пакета 69 | * С одной стороны, `Black Box` тесты проверяют реальные кейсы использования пакета, более 70 | стабильны и не требуют модификации при изменении внутренней логики работы 71 | * С другой стороны, `White Box` тесты позволяют производить тестирование более гранулярно и, например, 72 | менять внутренности имплементации для избежания внешних зависимостей (например, `timeFunc` 73 | в задаче `jwt`) 74 | 75 | ## External test packages 76 | 77 | * Тесты для пакета могут располагаться в отдельных пакетах (с именем `*_test`) 78 | * Это может быть полезно как для ограничения доступа к внутренностям пакета, так и для 79 | разрешения циклических зависимостей в тестах 80 | * Однако, когда мы пишем external test package для `White Box` тестов, возникает проблема с доступом 81 | к внутренним объявлениям пакета 82 | * В этом случае можно встретить файлы `export_test.go`, которые нужны для "экспортирования" 83 | внутренних сущностей в тесты ([пример в fmt](https://golang.org/src/fmt/export_test.go)) 84 | 85 | 86 | ## Полезные пакеты для тестирования 87 | 88 | * [stretchr/testify](https://github.com/stretchr/testify) - пакет, предоставляющий более удобные 89 | инструменты для написания тестов 90 | * [golang/mock](https://github.com/golang/mock) - пакет для генерации и использования моков в тестах 91 | * [uber-go/goleak](https://github.com/uber-go/goleak) - Goroutine leak detector 92 | * [DATA-DOG/go-sqlmock](https://github.com/DATA-DOG/go-sqlmock) - библиотека для написания моков к базам данных, 93 | работающим через `database/sql` 94 | * [httptest](https://golang.org/pkg/net/http/httptest/) - стандартный пакет, упрощающий тестирование 95 | http-сервисов 96 | 97 | ## Mocking 98 | 99 | * Зачастую в unit-тестах мы хотим протестировать определенную бизнес-логику "в вакууме" 100 | * В этом случае мы можем "подменить" реальную имплементацию чего-то пустышкой (удовлетворяющей 101 | нужному нам интерфейсу), которая будет всегда вести себя так, как мы запрограммировали 102 | * Например, мы можем исключить отправку нотификаций пользователям, лишь проверяя, что данный вызов 103 | состоялся 104 | * Или мы можем вместо использования временной базы описать ожидаемые запросы и результат их выполнения 105 | * В том числе, mocking позволяет избежать "флапающих" тестов (когда без каких-либо изменений 106 | тест то проходит, то нет) 107 | 108 | ## Code coverage 109 | 110 | * Одна из распространенных методик оценки "насколько хорошо протестирован пакет" 111 | * При запуске тестов просчитывается, какие строки кода исполнялись (и, возможно, сколько раз) 112 | * Хорошее покрытие кода тестами абсолютно не означает, что код не содержит багов 113 | * Тем не менее, хорошо покрытый тестами код с большей вероятностью работает стабильно и 114 | делает то, что от него ожидают, в важных кейсах 115 | * Зачастую в популярных пакетах можно встретить `code coverage` порядка 80-90%. Это отличный показатель, 116 | так как из-за явной обработки ошибок появляется много строк кода, исполнение которых маловероятно и тяжело для тестирования 117 | * Go имеет встроенную утилиту для работы с `code coverage`: `go tool cover` 118 | 119 | ## Benchmarks 120 | 121 | * Бенчмарк - функция в файлах `*_test.go`, начинающаяся на `Benchmark` и принимающая в 122 | аргументе `b *testing.B` 123 | * Объект аргумента предоставляет большинство методов, доступных в `testing.T`, дополняя 124 | их специфичными для бенчмарков методами и полями 125 | * Основное - `b.N` - количество итераций, которое нужно проделать 126 | * Чтобы запустить бенчмарки, необходимо передать в `go test` аргумент `-bench` с regexp 127 | бенчмарков, которые запустить (в случае `.` запускаются все бенчмарки пакета) 128 | * Зачастую бенчмарки испольуются для сравнения различных имплементаций или времени работы 129 | алгоритма в зависимости от размера входных данных 130 | * [https://pkg.go.dev/golang.org/x/tools/cmd/benchcmp](https://pkg.go.dev/golang.org/x/tools/cmd/benchcmp) - 131 | утилита, позволяющая сравнивать между собой различные запуски бенчмарков 132 | 133 | ## Profiling 134 | 135 | * Профилирование помогает найти узкие места в программе, которые можно оптимизировать 136 | * CPU profile - для запущенного процесса ОС с некоторой периодичностью (раз в несколько миллисекунд) 137 | прерывает исполнение и записывает, в каких местах исполнения находится программа 138 | * Heap profile - внутренняя функция выделения памяти записывает кто вызывает аллокации 139 | * Blocking profile - записываются все события блокировки горутин (на мьютексах, каналах, ...) 140 | * Обычно, профиль собирают на бенчмарках или на реальных программах, так как тесты зачастую 141 | заточены больше на проверку логики, чем на оценку эффективности работы 142 | * Для отображения профиля, помимо самого профиля, необходим бинарник с программой. В случае 143 | вызова `go test` с записью профиля остается собранный временный бинарник с тестами 144 | 145 | ## Примеры 146 | 147 | * Кроме тестов и бенчмарков, в `*_test.go` файлах располагаются примеры кода для документации 148 | * Это позволяет избежать ситуаций, когда примеры кода в документации устарели и не запускаются 149 | * Функция примера не принимает аргументов и начинается с `Example` 150 | * Посмотреть сгенерированную документацию можно через `go doc` 151 | * Если пример заканчивается на комментарий `// Output:`, то при тестировании будет вызвано 152 | сравнение вывода примера с каноничным -------------------------------------------------------------------------------- /lectures/05/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 5. Конкурентность и параллелизм. Горутины. Каналы 4 | 5 | ## Многопоточность 6 | 7 | * Последние годы (даже десятилетия) нет существенного прогресса в производительности процессоров (на одно ядро) 8 | * Даже в смартфонах процессоры имеют по несколько ядер 9 | * В случае пользовательских устройств многозадачность системы утилизирует все ядра даже в случае 10 | программ, написанных для запуска на одном ядре (однако такие программы, все равно, обычно используют несколько потоков) 11 | * В случае серверов, обслуживающих пользовательские запросы, обычно запущен один основной процесс, 12 | утилизирующий ресурсы по максимуму 13 | * Многопоточные программы позволяют утилизировать по максимуму мощность многоядерных процессоров, 14 | однако, писать корректные многопоточные программы значительно сложнее 15 | 16 | ## Конкурентность 17 | 18 | * В русской литературе, зачастую, не выделяют отдельно конкурентность, относя ее к параллелизму 19 | * Однако, конкурентность != параллелизм 20 | * Конкурентная программа может быть запущена на одном потоке, при этом работать производительнее 21 | аналогичной многопоточной программы (например, сервер, где много времени процесс ожидает данные по сети) 22 | * Go изначально проектировался так, что писать конкурентные (и многопоточные) программы в нем легко 23 | * Более того, сетевой стек написан с использованием асинхронных операций, что позволяет избежать 24 | ожидания сетевых данных, отдав исполнение другой части кода 25 | 26 | ## Конкурентное программирование в Go 27 | 28 | * Go предоставляет возможность использовать два стиля конкурентного программирования 29 | * Один из них - communicating sequential processes (CSP) - передача данных между независимыми "исполнителями" 30 | (горутинами) через методы взаимодействия (каналы) 31 | * Другой, более традиционный - shared memory multithreading - враимодействие конкурентных "исполнителей" 32 | с общей памятью (с синхронизацией доступа через примитивы синхронизации, такие как мьютекс и другие) 33 | 34 | ## Горутины 35 | 36 | * Горутина (goroutine) - отдельный "исполнитель" некоторого кода, независимый от других 37 | * В отличии от обычных функций (routine), горутины исполняются конкурентно (параллельно или чередуя исполнение) 38 | * Следует учитывать, что горутина - не то же самое, что поток (хотя сценарии использования очень похожие) 39 | * В любой программе есть, как минимум, одна горутина - в ней вызывается `main` 40 | * Чтобы вызвать функцию в отдельной горутине, достаточно перед вызовом функции добавить ключевое слово `go` 41 | 42 | go foo(42) 43 | 44 | ## Горутины 45 | 46 | * Несмотря на то, что у программы может быть несколько горутин в исполнении, как только завершается горутина 47 | с `main`, процесс завершается (не дожидаясь исполнения других горутин). Для ожидания можно использовать `sync.WaitGroup` 48 | * Для горутины нет никакого отдельного объекта, который бы позволял управлять ее (в частности, завершать ее исполнение). 49 | Горутина завершается (и освобождает выделенные на нее ресурсы) только в момент завершения функции, которая была в ней запущена 50 | * Существуют механизмы (например, пакет `context`), позволяющие сообщать в горутины (в которых, обычно, выполняются долгие операции, 51 | в реальных программах - в основном, ожидание чего то), что ее можно досрочно завершить 52 | 53 | ## Горутины 54 | 55 | * Фактически, горутина то же самое, что можно встретить в других языках программирования под 56 | названием сопрограмма (coroutine, корутина) 57 | * Основная особенность корутин - они могут прерывать свое исполнение и продолжать с того же места, как будто прерывания не было 58 | * Это позволяет отдавать исполнение (т.е. CPU) другой корутине, пока первая ожидает чего-то (например, I/O-операции или 59 | освобождения примитива синхронизации) 60 | * Для удобного использования корутин на уровне приложения существует scheduler (который работает независимо 61 | от системного), в случае горутинг этим занимается runtime Go. 62 | 63 | ## Горутины 64 | 65 | * При старте программы создается несколько потоков (указывается в переменной окружения `GOMAXPROCS`, по умолчанию - количество ядер), 66 | на которых запускаются горутины (параллельно в случае нескольких потоков) 67 | * Горутины могут переключаться (вызов функции, блокировка, вызов `runtime.Gosched`), давая исполнение другим горутинам 68 | * В большинстве случаев, это выглядит как "параллельное" исполнение множества горутин, однако, можно 69 | написать горутину так, что она не будет снята с исполнения длительное время (в ней не будет точек переключения) 70 | * Например, это могут быть какие-то численные вычисления с множеством циклов, не вызывающие другие функции 71 | * В этом случае, рекомендуется, вручную вызывать `runtime.Gosched` 72 | 73 | ## Горутины 74 | 75 | * У каждой горутины существует свой стек исполнения, равный нескольким килобайтам 76 | * Переключение горутины для исполнения на потоке - операция, сильно более дешевая, чем переключение системного потока 77 | * Таким образом, для программы на Go нормальная ситуация, когда одновременно существуют сотни и даже тысячи горутин 78 | (в случае такого количества потоков процесс может сильно тормозить из-за оверхэда на переключение контекстов) 79 | * Подробнее: [раз](https://habr.com/ru/post/141853/), [два](https://habr.com/ru/company/otus/blog/527748/) 80 | * [Про асинхронность и корутины в C++](https://habr.com/ru/post/201826/) 81 | 82 | ## Каналы 83 | 84 | * Каналы позволяют нескольким горутинам обмениваться информацией (но не только) 85 | * В канал можно что-то записать и прочитать (потокобезопасно) 86 | * Канал типизирован (так же, как `map` и `slice`), т.е. можно передавать объекты только определенного типа 87 | * Тип канала - `chan T`, где `T` - некоторый тип. Канал должен создаваться через `make` 88 | 89 | ch := make(chan int) 90 | 91 | * Также существуют типы однонаправленных каналов (`<-chan T`, `chan<- T`) - можно контролировать, 92 | что из канала никто не будет читать либо что в канал никто не будет писать 93 | * Как и в случае с `map`, канал - reference type, т.е. при передаче куда либо он будет передаваться "по ссылке" 94 | 95 | ## Каналы 96 | 97 | * Zero value для канала - nil 98 | * Каналы могут сравниваться на равенство между собой и с nil 99 | * В канал можно записать значение и прочитать значение из него 100 | 101 | val := <-ch 102 | ch <- newVal 103 | 104 | * Кроме этого, канал можно закрыть (`close(ch)`), при этом в канал нельзя писать (вызовет панику), 105 | но можно читать - будет возвращено zero value 106 | * При чтении из канала можно получить вторым аргументом, было ли прочитано значение 107 | 108 | val, ok := <-ch 109 | 110 | ## Каналы 111 | 112 | * Канал может быть буферизированным или нет 113 | * Для создания буферизированного канала необходимо в `make` вторым аргументом передать размер буфера 114 | * Небуферизированный канал - буфер размера 0 115 | * Операции с каналами могут быть блокирующими: запись в канал сверх буфера заблокируется до того момента, 116 | пока кто-то не прочитает значение из канала; чтение из пустого канала (никто не пишет в него и ничего нет в буфере) 117 | тоже заблокирует выполнение 118 | * Небуферизированные каналы можно использовать для синхронизации каких-либо дуйствий в разных горутинах 119 | (т.к. для разблокировки операции нужно, чтобы одновременно кто-то читал и писал) 120 | 121 | ## Каналы 122 | 123 | * Зачастую каналы используются для составления пайплайнов (pipeline): несколько горутин принимают 124 | значения из канала и передают в другой канал, выстраивая цепочку обработки 125 | * При этом, самое медленное звено в цепочке определяет скорость обработки данных 126 | * Но можно сделать несколько параллельных звеньев, которые будут читать из одной очереди и писать в одну очередь 127 | 128 | 129 | ## Каналы 130 | 131 | * По значениям в канале можно итерироваться через range 132 | 133 | for val := range ch {...} 134 | // Эквивалентно 135 | for { 136 | val, ok := <-ch 137 | if !ok { 138 | break 139 | } 140 | ... 141 | } 142 | 143 | * Единственный способ выйти из такого цикла - закрыть канал (обычно, закрывает кто-то снаружи) 144 | 145 | ## Мультиплексирование 146 | 147 | * Мультиплексирование позволяет объединять чтение (или запись) для нескольких каналов 148 | * Из-за удобства этого механизма некоторые сущности, не связанные с коммуникацией, используют каналы 149 | (`context.Done`, `time.After`) 150 | * Кроме этого, данный механизмм позволяет прочитать или записать что-то без блокировки (т.е. в случае, если бы 151 | случилась блокировка, мы выйдем, ничего не записав и не прочитав) 152 | 153 | select { 154 | case <-ch: 155 | // ... 156 | case <-ch2: 157 | // ... 158 | default: 159 | // ... 160 | } 161 | 162 | ## Отмена исполнения горутин 163 | 164 | * Есть несколько способов (фактически, один, но разными сущностями) правильно отменить исполнение горутины 165 | * Один - передать канал, из которого в select дожидаться сообщения о том, что можно завершиться 166 | * Недостаток - сложно отменить некоторые потенциально долгие вещи (такие, как http-запрос) 167 | * Другой способ - передавать контекст (пакет `context`) 168 | * Две ключевых роли контекста - передача сигнала об отмене и передача данных далее при обработке (например, пользовательского запроса) 169 | -------------------------------------------------------------------------------- /lectures/02/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 2. Базовые конструкции языка 4 | 5 | ## Имена переменных, констант, функций, ... 6 | 7 | * Должны начинаться с буквы (unicode) или символа `_` 8 | * Могут содержать в себе буквы (unicode), цифры и символ `_` 9 | * Регистр имеет значение (`hello` и `Hello` - разные имена) 10 | * Регистр первой буквы влияет на область видимости глобальных переменных, констант, функций и типов: 11 | 12 | var Foo = "foo" // можно обращаться из других пакетов 13 | var bar = "bar" // можно обращаться только из того же пакета 14 | 15 | * Имена не имеют ограничений на длину, однако, принято, чтобы маложивущие объекты имели краткое и лаконичное название 16 | * Для именования используется `CamelCase`, `snake_case` допустим синтаксически, но никто его не использует 17 | * Сокращения аббревиатур: `htmlTags` (не `hTMLTags`), `HTMLTags` (не `HtmlTags`), `marshalJSON` (не `marshalJson`) 18 | 19 | ## Ключевые слова 20 | 21 | * Ключевые слова не могут быть использованы в качестве имен: 22 | 23 | break default func interface select 24 | case defer go map struct 25 | chan else goto package switch 26 | const fallthrough if range type 27 | continue for import return var 28 | 29 | * Кроме этого, есть `predeclared` имена, которые могут использоваться в качестве имен других объектов 30 | 31 | true false iota nil 32 | 33 | int int8 int16 int32 int64 34 | uint uint8 uint16 uint32 uint64 35 | uintptr float32 float64 complex128 36 | complex64 bool byte 37 | rune string error 38 | 39 | make len cap new append copy close delete complex real imag 40 | panic recover 41 | 42 | ## Объявления 43 | 44 | * `var` - объявление переменной 45 | * `const` - объявление константы 46 | * `type` - объявление пользовательского типа 47 | * `func` - объявление функции или метода 48 | 49 | ## Переменные 50 | 51 | var name type = expression 52 | 53 | * `type` или `expression` могут отсутствовать, но не оба одновременно 54 | * При отсутствии инициализации, переменная инициализируется `zero value` (0, "", nil и т.д.) 55 | * Можно объявить несколько переменных (и в том числе инициализировать их): 56 | 57 | var i, j int = 1, 2 58 | var name, value = "number", 42 // типы могут быть разными, но только если они выводятся 59 | 60 | * Глобальные переменные инициализируются (и выделяются в памяти) до вызова `main`, 61 | локальные переменные инициализируются в момент выполнения их определения 62 | * Несколько переменных могут быть инициализированы результатом вызова функции: 63 | 64 | var f, err = os.Open("filename.txt") 65 | 66 | ## Краткий синтаксис объявления переменных 67 | 68 | * Если объявление переменной происходит совместно с ее инициализацией, то можно использовать краткий синтаксис: 69 | 70 | name := expression // var name = expression 71 | 72 | * Краткий синтаксис не может быть использован для глобальных переменных 73 | * Хотя бы одна из переменных должна быть необъявленной, в этом случае для уже объявленных переменных 74 | будет вызвано просто присваивание 75 | 76 | i, j := 0, 1 // ok 77 | i, j := 2, 3 // error, i and j already declared 78 | x, i := 2, 3 // ok 79 | i, j = 2, 3 // ok 80 | 81 | * Обычно для переменных, инициализируемых `zero value` (т.е. без инициализации) используют синтаксис с `var` 82 | 83 | ## Указатели 84 | 85 | * Указатель хранит адрес некоторой области в памяти (почти всегда - переменной) 86 | * `&` - взятие адреса, `*` - разыменование адреса 87 | * Для типа `T` тип указателя на переменную этого типа - `*T` (может быть указатель на указатель и т.д.) 88 | * Указатели можно сравнивать, но не выполнять арифметические операции 89 | * С некоторым упрощением указатели - это другие имена для одной и той же переменной, изменение через указатель 90 | меняет значение исходной переменной 91 | * Так как аргументы функции всегда передаются по значению, то чтобы функция могла изменять переменную вовне 92 | надо передать указатель 93 | 94 | ## Функция `new` 95 | 96 | * Принимает в качестве аргумента тип, создает неименованную переменную этого типа и возвращает на нее указатель 97 | 98 | x := new(int) // то же самое, что var dummy int; x = &dummy 99 | 100 | * Функция `new` всегда возвращает адрес новой переменной (возможное исключение - пустые типы размера 0, 101 | например `struct{}` или `[0]int`) 102 | 103 | ## Время жизни переменных 104 | 105 | * Для глобальных переменных - все время исполнения программы 106 | * Для локальных переменных - с момента объявления и до того момента, пока не останется ни одной переменной 107 | и ни одного указателя, которые бы ссылались на эту переменную 108 | * В отличии C++, возврат указателя на локальную переменную - валидная конструкция. В этом случае 109 | переменная живет до тех пор, пока живет указатель на нее 110 | * В зависимости от того, выходит ли переменная за пределы функции или нет, память под нее может быть выделена 111 | в куче или на стеке (это решает `escape analysis` на этапе компиляции) 112 | 113 | var global *int 114 | 115 | func f() { func g() { 116 | var x int y := new(int) // local 117 | x = 1 *y = 1 118 | global = &x // escape } 119 | } 120 | 121 | ## Присваивание 122 | 123 | * Оператор присваивания `=` 124 | * Можно присваивать несколько переменных (как и в случае объявления переменных) 125 | * Значения с правой стороны вычисляются до того, как будет изменена хотя бы одна 126 | переменная слева (`x, y = y, x%y`) 127 | * В случае, если нужно пропустить одно из присваиваний, можно использовать `_` 128 | 129 | if _, found := values[key]; found { 130 | // ... 131 | } 132 | 133 | ## Определение типа 134 | 135 | * Go позволяет определить новый тип на основе какого-либо другого (при этом это абсолютно другой тип, приведение - `T(val)`) 136 | 137 | type name underlying-type 138 | 139 | * Можно определять типы и внутри функций, но обычно они встречаются на уровне пакета 140 | 141 | .code code/tempconv/tempconv.go 142 | 143 | ## Определение типа 144 | 145 | * Типы, которые основаны на встроенных типах, можно приводить 146 | к этому типу и между собой (однако само значение не изменяется при приведении!) 147 | * Операции над встроенными типами наследуются 148 | * Для новых типов можно определять методы, добавляя новое поведение 149 | 150 | ## Пакеты и импорты 151 | 152 | * Имя пакета, обычно, не содержит символов `_` (например, `httprequest`) 153 | * Экспортируемые объекты (переменные, константы, типы, функции) должны начинаться с большой буквы 154 | * У каждого пакета может быть любое количество функций инициализации 155 | 156 | func init() { 157 | // ... 158 | } 159 | 160 | * Функции инициализации вызываются при инициализации пакета и не могут быть вызваны вручную 161 | * Если пакет `a` импортирует пакет `b`, то в момент инициализации `a` можно быть уверенным, 162 | что `b` уже инициализирован 163 | * Циклические импорты запрещены 164 | 165 | ## Область видимости 166 | 167 | * Области видимости ограничены блоками (в том числе `{` и `}` ) 168 | * Объявление внутри области видимости не видны снаружи, но видны во вложенных областях видимости 169 | * Объекты могут иметь одни и те же имена, если располагаются в разных областях видимости 170 | * Вложенная область видимости может "перекрывать" (`shadow`) имена из родительской 171 | области видимости (нужно быть осторожным с `:=`) 172 | 173 | var y = "hello" 174 | { 175 | x, y := "1", "2" 176 | fmt.Println(x, y) 177 | } 178 | fmt.Println(y) 179 | 180 | ## Базовые типы 181 | 182 | * Список встроенных типов 183 | 184 | int int8 int16 int32 int64 185 | uint uint8 uint16 uint32 uint64 186 | uintptr float32 float64 complex128 187 | complex64 bool byte 188 | rune string error 189 | 190 | * Операции над встроенными типами (первые две строки имееют кратную форму, например, `+=`) 191 | 192 | * / % << >> & &^ 193 | + - | ^ 194 | == != < <= > >= 195 | && 196 | || 197 | 198 | * `^` - XOR (бинарный) или NOT (унарный), а `&^` - AND NOT 199 | * Автоматическое выведение типа выводит всегда один и то же тип (для целых чисел - int), 200 | нужно явно указывать тип чтобы получить, например, uint16 (`var x uint16 = 10`) 201 | 202 | ## Строки 203 | 204 | * Строка - неизменяемая последовательность байт. Она может содержать произвольные данные (включая \0), 205 | однако, обычно в строках содержат человекочитаемый текст 206 | * Строки интерпретируются в кодировке utf8 (поэтому нет типа `char`, а есть `rune`) 207 | * `str[i]` позволяет получить доступ к i-му байту (но это не обязательно i-й символ!) 208 | * `str[i:j]` позволяет получить подстроку с байтами с i-го по j-й (левая и правая границы могут быть опущены) 209 | * "\n" - строка с переносом строки, `\n` - строка из двух символов \ и n 210 | * Для работы с unicode-строками есть пакет "unicode" (и вложенные в него пакеты) 211 | * Строки могут быть приведены к `[]byte` и наоборот, для приведения к числам существует пакет `strconv` 212 | 213 | ## Константы 214 | 215 | * Константы могут быть объявлены с помощью ключевого слова const 216 | 217 | const pi = 3.14 218 | const ( 219 | x = 10 220 | y 221 | num = 42 222 | ) 223 | 224 | * Константами могут быть встроенные типы и пользовательские типы, основанные на встроенных 225 | * Тип для констант может быть выведен сам с максимально компактным представлением 226 | (при этом в местах использования не надо выполнять приведение типа) 227 | * Есть в блоке констант опущена правая часть, то очередная константа получает значение предыдущей 228 | 229 | ## iota 230 | 231 | * `iota` - константный генератор, начинает с 0 и увеличивается для каждой следующей константы 232 | 233 | type Weekday int 234 | const ( 235 | Sunday Weekday = iota 236 | Monday 237 | Tuesday 238 | ... 239 | ) 240 | 241 | const ( 242 | _ = 1 << (10 * iota) 243 | KiB // 1024 244 | MiB // 1048576 245 | ) -------------------------------------------------------------------------------- /lectures/04/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 4. Defer и Panic. Интерфейсы. Ошибки 4 | 5 | ## Defer 6 | 7 | * Механизм отложенного вызова некоторой функции (или метода) при выходе из другой функции 8 | * Позволяет избежать дублирований кода (особенно при явной обработке ошибок) 9 | 10 | var m sync.Mutex 11 | m.Lock() 12 | if err := foo(); err != nil { 13 | m.Unlock() // unlock on error 14 | return err 15 | } 16 | // ... 17 | if err := bar(); err != nil { 18 | m.Unlock() // unlock on error 19 | return err 20 | } 21 | 22 | ## Defer 23 | 24 | * Мы можем сказать, что некоторая функция должна быть вызвана при любом раскладе при выходе из функции (даже при панике) 25 | 26 | m.Lock() 27 | defer m.Unlock() 28 | // ... 29 | if err != nil { 30 | return err // будет вызван Unlock при выходе из функции 31 | } 32 | 33 | * Сам вызов происходит при выходе из функции, однако аргументы рассчитываются в момент объявления defer 34 | * Функция может иметь множество объявлений defer, и гарантируется, что они будут позваны в обратном порядке их объявлению 35 | * defer имеет отношение именно к функции (defer в if, for и прочих равносилен другим объявлениям) 36 | * Обычно defer используется для освобождения некоторых ресурсов и объявляется сразу после захвата 37 | 38 | ## Defer 39 | 40 | * Кроме освобождения ресурсов, defer может быть полезен для выполнения различных действий при выходе из функции 41 | 42 | func bigSlowOperation() { 43 | defer trace("bigSlowOperation")() // don't forget the extra parentheses 44 | // ...lots of work... 45 | time.Sleep(10 * time.Second) // simulate slow operation by sleeping 46 | } 47 | func trace(msg string) func() { 48 | start := time.Now() 49 | log.Printf("enter %s", msg) 50 | return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) } 51 | } 52 | 53 | * Также defer может использоваться для модификации возвращаемых значений (для этого они должны быть именованными) 54 | 55 | ## Panic 56 | 57 | * Экстренное завершение нормального исполнения программы с досрочным выходом из функций (пока не будет поймано) 58 | * Зачастую это ситуации, когда дальнейшее выполнение может быть некорректно (например, разыменование nil, неправильное приведение типа, ...) 59 | * По своей природе сильно похож на исключения в других языках (но исключения и паники используются в абсолютно разных контекстах) 60 | * При выходе из функции в процессе паники вызываются все defer'ы для этой функции 61 | * В случае, если паника не поймана, она приводит к завершению выполнения программы (и печатает лог для разбора) 62 | * Панику можно кинуть самому с помощью функции `panic` (принимает один аргумент любого типа) 63 | 64 | ## Panic 65 | 66 | * Паники зачастую используют для проверки логической неконсистентности программы (что в других языках делается через assert) 67 | * "Ожидаемые" ошибки (сетевые, файловой системы, валидации, ...) не должны приводить к паникам 68 | * Также паники бывают в ситуациях, когда теоретически ошибка может быть, но практически (с точки зрения логики кода) ее быть не должно (см. `regexp.MustCompile`) 69 | 70 | ## Recover 71 | 72 | * Позволяет прекратить выполнение паники и получить переданный в нее объект 73 | * Вызов `recover` вне ситуации паники возвращает nil 74 | * Полезна в ситуациях, когда мы не хотим экстренно завершать программу (например, обработка одного запроса web-сервера спаниковала, но мы не хотим складывать весь сервис) 75 | * Так как в процессе паники вызываются только `defer`'ы, то `recover` имеет смысл только внутри `defer'а` 76 | * Есть плохая, но встречающаяся практика, когда с помощью паники раскручивают стек рекурсивных вызовов до первого, где панику ловят и возвращают обычную ошибку 77 | 78 | ## Методы 79 | 80 | * Фактически, метод - обычная функция в выделенным отдельно аргументом (объявляется до имени функции) 81 | 82 | type Point struct{ X, Y float64 } 83 | // traditional function 84 | func Distance(p, q Point) float64 { 85 | return math.Hypot(q.X-p.X, q.Y-p.Y) 86 | } 87 | // same thing, but as a method of the Point type 88 | func (p Point) Distance(q Point) float64 { 89 | return math.Hypot(q.X-p.X, q.Y-p.Y) 90 | } 91 | 92 | * Объект вызова метода - method receiver 93 | * Receiver может иметь любое имя (более того, имена `this` или `self` считается плохим тоном) 94 | * Часто можно встретить, что receiver называется по первой букве типа 95 | 96 | ## Методы 97 | 98 | * Для каждого типа свой namespace для имен методов, так что они не конфликтуют с функциями и методами других типов 99 | * Метод может быть у любого типа (не только структуры) 100 | 101 | type Path []Point 102 | func (p Path) Distance() float64 { 103 | ... 104 | } 105 | 106 | * Методы могут быть объявлены только в рамках того же пакета 107 | * Receiver такой же аргумент, как и другие, так что он передается по значению в примере выше 108 | 109 | ## Методы 110 | 111 | * Метод можно привязать не к самому типу, а к типу указателя 112 | * В этом случае появляется возможность модифицировать receiver 113 | * По соглашению, если есть хотя бы один pointer receiver, то все методы объявляются с pointer receiver 114 | * Запрещено объявлять методы для типов, которые являются алиасами на указатели 115 | 116 | type P *int 117 | 118 | func (p P) foo() { // compile error: invalid receiver type P (P is a pointer type) 119 | } 120 | 121 | * Метод с pointer receiver можно вызывать как на указателе, так и на самом объекте (будет неявное взятие адреса) 122 | * Однако, нельзя позвать такой метод для значения, от которого нельзя взять адрес (например, значение в map через []) 123 | 124 | ## Методы 125 | 126 | * Методы, имеющие receiver по значению, мы можем позвать и для указателей (так как всегда можно получить значение по указателю) 127 | * Так как receiver участвует как обычный аргумент, он вполне может быть `nil`, и это не будет ошибкой (некоторые библиотеки обрабатывают это) 128 | * При встраивании типов доступен сокращенный синтаксис вызова методов (по аналогии с полями) 129 | * Можно использовать методы как значения (передавать в функции и т.д.) 130 | * Выражение `p.Distance` (method value) возвращает функцию, привязанную к `p` (т.е. нет необходимости вызывать ее как метод) 131 | * Выражение `Point.Distance` (method expression) возвращает функцию, где receiver становится первым аргументом функции 132 | * Все те же правила для имен экспортируемых/неэкспортируемых сущностей применимы и для методов 133 | 134 | ## Интерфейсы 135 | 136 | * Механизм обобщения (generalization) в Go 137 | * Применяется "утиная типизация" - нет необходимости явно указывать, какие интерфейсы имплементирует данный тип (в отличии от многих других языков) 138 | * Интерфейс - абстрактный тип, он только описывает набор методов (и никак не связан с имплементацией) 139 | * Если мы работаем с значением типа интерфейс, мы только можем знать, что объект может делать, но ни чем он является (но можем предполагать, об этом отдельно :) ) 140 | 141 | ## Интерфейсы 142 | 143 | * В Go интерфейсы, обычно, имеют достаточно малый набор методов (см. интерфейсы стандартной библиотеки) 144 | * Можно воспринимать интерфейс как контракт между вызывающей стороной и вызываемой 145 | * При этом, вызываемой стороне неважно, какой тип скрывается за интерфейсом - важно лишь наличие нужных методов 146 | 147 | package fmt 148 | 149 | func Fprintf(w io.Writer, format string, args ...interface{}) (int, error) 150 | 151 | func Printf(format string, args ...interface{}) (int, error) { 152 | return Fprintf(os.Stdout, format, args...) 153 | } 154 | 155 | func Sprintf(format string, args ...interface{}) string { 156 | var buf bytes.Buffer 157 | Fprintf(&buf, format, args...) 158 | return buf.String() 159 | } 160 | 161 | ## Интерфейсы 162 | 163 | * Среди интерфейсов стандартной библиотеки можно выделить `io.Writer`, `io.Reader`, 164 | `io.Closer`, их комбинации (похоже на встраивание), `fmt.Stringer` 165 | 166 | 167 | type ReadWriter interface { 168 | Reader 169 | Writer 170 | } 171 | 172 | * Pointer receiver имеет важное значение для интерфейсов. Например в ситуации, 173 | 174 | func (p *Point) String() string {...} 175 | 176 | только указатель на Point будет удовлетворять интерфейсу `fmt.Stringer` 177 | 178 | * Хорошим тоном считается имплементировать методы стандартных интерфейсов вместо использования своих сигнатур 179 | * Зачастую можно встретить подход: функция возвращает как можно больше (конкретный тип), но требует как можно меньше (интерфейс) 180 | 181 | 182 | ## Интерфейсы 183 | 184 | * Из-за отсутствия в языке поддержки дженериков (которые скоро будут!), в большом количестве мест встречается пустой интерфейс `interface{}` - ему удовлетворяет любой тип 185 | * Фактически, интерфейс - структура с двумя полями: type и value. Zero value для интерфейса - оба поля nil 186 | * Динамический тип определяет, является ли интерфейс nil или нет 187 | * Вызов метода на nil-интерфейсе вызывает панику 188 | * Нужно понимать, что nil-интерфейс и интерфейс, содержащий nil (т.е. имеющий динамический тип) - разные понятия 189 | * Интерфейсы можно сравнивать между собой - они равны, если имеют одинаковые значения и одинаковый динамический тип 190 | 191 | ## Type assertion 192 | 193 | * Если мы знаем, какой тип скрывается за интерфейсом (или, например, что скрывается тип, который удовлетворяет большему интерфейсу) 194 | мы можем привести значение к этому типу 195 | * Этот механизм используется, в том числе, для получения конкретных ошибок из интерфейса `error` 196 | 197 | var w io.Writer 198 | w = os.Stdout 199 | f := w.(*os.File) // success: f == os.Stdout 200 | c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer 201 | c, ok := w.(*bytes.Buffer) // don't panic, but ok == false 202 | 203 | ## Type switch 204 | 205 | * Позволяет выбрать из предположений нужный тип. Например, может использоваться при написании `Printf` 206 | 207 | switch x.(type) { 208 | case nil: // ... 209 | case int, uint: // ... 210 | case bool: // ... 211 | case string: // ... 212 | default: // ... 213 | } 214 | 215 | ## Ошибки 216 | 217 | * Все ошибки возвращаются явным образом с типом встроенного интерфейса `error` (исключение - panic, о нем отдельно) 218 | 219 | type error interface { 220 | Error() string 221 | } 222 | 223 | * Можно создавать ошибки различных типов, главное, чтобы они удовлетворяли интерфейсу 224 | * Ошибки можно сравнивать с nil и между собой (устаревший вариант, после go 1.13 для сравнения используются `errors.Is` и `errors.As`) 225 | * Метод `errors.Unwrap` вызывает соответствующий метод у типа, если он определен, либо возвращает nil 226 | 227 | ## Ошибки 228 | 229 | * Ошибки могут оборачивать одна другую: `fmt.Errorf("failed: %w", ErrNotFound)` 230 | * `errors.Is` - в цепочке ошибок есть ошибка-переменная (`var ErrNotFound = errors.New("not found")`) 231 | * `errors.As` - в цепочке ошибок есть ошибки с нужным типом -------------------------------------------------------------------------------- /lectures/03/lecture.slide: -------------------------------------------------------------------------------- 1 | # Программирование на языке Go 2 | 3 | Лекция 3. Составные типы. Ошибки 4 | 5 | ## Массивы 6 | 7 | * Последовательность элементов определенного типа фиксированной длины (>=0) 8 | * В основном используются слайсы, которые построены на массивах 9 | * `[2]int` и `[3]int` - два разных типа. Размер должен быть константным выражением 10 | * Поддерживаются операции доступа по индексу и итерация через `range` 11 | 12 | var a [3]int 13 | fmt.Println(a[0]) 14 | fmt.Println(a[len(a)-1]) 15 | for i, v := range a {...} 16 | 17 | * Возможна инициализация через `{}`, в том числе с указанием индексов 18 | 19 | var q [3]int = [3]int{1, 2, 3} 20 | var p = [...]int{1, 2, 3} 21 | symbol := [...]string{USD: "$", EUR: "9", GBP: "!", RMB: """} 22 | fmt.Println(RMB, symbol[RMB]) 23 | 24 | ## Массивы 25 | 26 | * Если элементы массива сравнимы, то сравнимы и массивы 27 | * Передача массива в функцию приводит к копированию всех элементов, что неэффективно для массивов большого размера 28 | 29 | ## Слайсы 30 | 31 | * Последовательность элементов определенного типа изменяемой длины 32 | * Данные на самом деле хранятся в массиве, который реаллоцируется при заполнении 33 | * Фактически, это структура, хранящая указатель на массив, текущую длину и максимальную вместительность 34 | 35 | type slice struct { 36 | array unsafe.Pointer 37 | len int 38 | cap int 39 | } 40 | 41 | s := make([]int, 0, 3) 42 | len(s) // 0 43 | cap(s) // 3 44 | 45 | * При передаче в функцию копируется структура `slice`, но указатель ссылается на те же данные. 46 | Это приводит к набору спецэффектов, которые надо иметь ввиду 47 | 48 | ## Слайсы 49 | 50 | * Несколько слайсов могут ссылать на один и тот же массив (т.е. изменение элемента через любой из 51 | этих слайсов видно остальным) 52 | * Оператор `[i:j]`, `0 <= i <= j <= cap(s)` создает новый слайс, который начинается с iго элемента 53 | родительского слайса и имеет соотвествующие длину и capacity 54 | 55 | s := []int{0, 1, 2} 56 | s2 := s[1:] 57 | s2[0] = 3 58 | fmt.Println(s) // [0, 3, 2] 59 | 60 | * Сабслайс с границей большей чем `cap(s)` приводит к панике, но с границей, 61 | большей `len(s)` валидно (так что сабслайс может быть больше, чем исходный) 62 | * Единственное валидное сравнение для слайса через `==` - сравнение с nil 63 | * Слайс, инициализированный nil - валидный для добавления элементов слайс длины 0 и capacity 0. 64 | Однако, обратное не верно - пустой слайс не то же самое, что nil 65 | 66 | ## append 67 | 68 | * Встроенная функция `append` принимает слайс и элементы и возвращает новый слайс (т.е. `type slice`) с добавленными элементами в конце 69 | * append может как вызвать реаллокацию исходного массива (и тогда исходный слайс и новый будут иметь под собой разные массивы), 70 | так и возвращать новый слайс на основе того же массива 71 | * Об этом надо всегда помнить, так как иначе можно получить неожиданное поведение 72 | 73 | ## append 74 | 75 | func appendInt(x []int, y int) []int { 76 | var z []int 77 | zlen := len(x) + 1 78 | if zlen <= cap(x) { 79 | // There is room to grow. Extend the slice. 80 | z = x[:zlen] 81 | } else { 82 | // There is insufficient space. Allocate a new array. 83 | // Grow by doubling, for amortized linear complexity. 84 | zcap := zlen 85 | if zcap < 2*len(x) { 86 | zcap = 2 * len(x) 87 | } 88 | z = make([]int, zlen, zcap) 89 | copy(z, x) // a built-in function; see text 90 | } 91 | z[len(x)] = y 92 | return z 93 | } 94 | 95 | ## In-Place операции 96 | 97 | * Многие операции над слайсами (в основном фильтрацию) можно выполнить in-place, что будет эффективнее, чем аллоцировать 98 | место под новые элементы 99 | 100 | var a = []int{0, 1, -1, 2, -2} 101 | filtered := a[:0] // len=0, cap=5 102 | for _, el := range a { 103 | if el > 0 { 104 | filtered = append(filtered, el) 105 | } 106 | } 107 | fmt.Println(filtered, len(filtered), cap(filtered)) // [1 2] 2 5 108 | fmt.Println(a, len(a), cap(a)) // [1 2 -1 2 -2] 5 5 109 | 110 | ## In-Place операции 111 | 112 | * Для удаление элемента из начала или конца можно использовать `[i:j]`. При следующей реаллокации 113 | "забытые" элементы не копируются 114 | 115 | func push(q []int, el int) []int { 116 | return append(q, el) 117 | } 118 | 119 | func pop(q []int) []int { 120 | return q[1:] 121 | } 122 | 123 | var q []int // [] 0 0 124 | q = push(q, 1) // [1] 1 1 125 | q = push(q, 2) // [1 2] 2 2 126 | q = pop(q) // [2] 1 1 127 | q = push(q, 3) // [2 3] 2 2 128 | 129 | ## Другие операции над слайсами 130 | 131 | * Удаление и вставка в середину - дорогая операция 132 | 133 | var a = []int{0, 1, 3, 5} 134 | a = append(a[:2+1], a[2:]...) 135 | a[2] = 2 136 | fmt.Println(a) // [0 1 2 3 5] 137 | 138 | var a = []int{0, 1, 2, 3, 5} 139 | a = append(a[:2], a[3:]...) 140 | fmt.Println(a) // [0 1 3 5] 141 | 142 | ## Ассоциативные массивы (maps) 143 | 144 | * Unordered ассоциативный массив на основе hash-таблицы 145 | * Аналогично слайсам, map - структура, хранящая указатели данные 146 | * Однако, операции над map более инкапсулированы 147 | 148 | type hmap struct { 149 | // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. 150 | // Make sure this stays in sync with the compiler's definition. 151 | count int // # live cells == size of map. Must be first (used by len() builtin) 152 | flags uint8 153 | B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) 154 | noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details 155 | hash0 uint32 // hash seed 156 | 157 | buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. 158 | oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing 159 | nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) 160 | 161 | extra *mapextra // optional fields 162 | } 163 | 164 | ## Maps 165 | 166 | * Ключ должен быть сравним через оператор `==` (однако, использовать в качестве ключа `float` типы зачастую плохая идея) 167 | * Для использования map необходимо инициализировать через `make` или `{}`. В противном случае при попытке вставить элемент 168 | будет panic (в отличии от слайсов, где nil - валидный слайс) 169 | 170 | var m = make(map[string]int) 171 | var m2 = map[string]int{ 172 | "one": 1, 173 | } 174 | 175 | * map можно сравнивать (==) с nil 176 | * При обращении к несуществующему элементу будет возвращено zero value и (необязательный) флаг наличия элемента 177 | 178 | one := m["one"] 179 | one, ok := m["one"] 180 | 181 | * Для удаления элемента из map есть встроенная функция delete 182 | 183 | ## Maps 184 | 185 | * С элементами map можно производить операции как с обычными переменными, за исключением того, что 186 | от элемента нельзя взять адрес 187 | 188 | _ = &ages["bob"] // compile error: cannot take address of map element 189 | 190 | * Для итерации можно использовать `for range` 191 | 192 | for name, age := range ages { 193 | fmt.Printf("%s\t%d\n", name, age) 194 | } 195 | 196 | * Порядок элементов не определен, если необходим отсортированный результат - придется 197 | складывать элементы в слайс и сортировать его 198 | * В Go нет типа set, поэтому используется map вместо него 199 | 200 | var set map[string]bool 201 | var set2 map[string]struct{} // struct{} - empty type 202 | 203 | ## Структуры 204 | 205 | * Объединение нескольких именованных перменных (полей) различных типов 206 | * Доступ до различных элементов - через `.` 207 | 208 | type Employee struct { 209 | ID int 210 | Name string 211 | } 212 | var dilbert Employee 213 | dilbert.ID = 1 214 | 215 | * Можно брать указатели на отдельные поля 216 | * Неявное разыменование - отсутствует оператор `->` 217 | 218 | var ptr = &dilbert 219 | ptr.ID = 2 220 | (*ptr).ID = 3 221 | 222 | * Обычно поля объявляются по одному на строку, но можно объединять поля одного типа (как в `var`) 223 | * Первая бука поля (заглавная или нет) определяет видимость этого поля из других пакетов 224 | 225 | ## Структуры 226 | 227 | * Zero value для структуры - комбинация zero values его полей 228 | * Обычно, если у пакета нет функции `NewSmth` для типа `Smth`, то валидно использовать zero value этого типа 229 | * `struct{}` - пустая структура размера 0 (иногда используется в map) 230 | * Структура может быть инициализирована через `{}` с указанием имен полей и без (не рекомендуется) 231 | 232 | type Point { X, Y int } 233 | var p = Point{1, 2} 234 | var p2 = Point{X: 1, Y: 2} 235 | var p3 = Point{Y: 2, X: 1} 236 | var p4 = Point{Y: 2} // X = zero value 237 | 238 | * Зачастую используются указатели на структуры, при инициализации можно сразу получить указатель 239 | 240 | var ptr = &Point{} 241 | 242 | * Если все поля структуры сравнимы, то и структуру можно сравнивать с использованием `==` и `!=` 243 | 244 | ## Embedding 245 | 246 | * "Своеобразное" наследование, позволяет указать другую структуру анонимным полем и обращаться через `.` напрямую к полям (и методам) анонимного поля 247 | 248 | type Circle struct { 249 | Point // Point Point 250 | Radius int 251 | } 252 | var c = Circle{Point: Point{1, 2}, Radius: 0} 253 | c.X = 0 254 | c.Point.Y = 0 255 | 256 | * Нельзя иметь два анонимных поля одного типа 257 | * Видимость анонимного поля определяется видимостью имени типа (но сокращения через `.` все равно продолжат работать) 258 | * Если возникает конфликт по именам полей двух или более анонимных полей, то краткая форма через `.` недоступна 259 | * К полям можно навешивать тэги, которые используются некоторыми библиотеками (например, json) 260 | 261 | ## Функции 262 | 263 | func name(parameter-list) (result-list) { 264 | body 265 | } 266 | 267 | * Функция может принимать >= 0 аргументов и возвращать >= 0 значений 268 | * Обычно аргументам даются имена (однако, можно их опускать или называть `_`) и редко даются имена 269 | возвращаемым значениями (в основном, когда из типов сразу не ясно, что возвращается) 270 | * При именованных возвращаемых значениях при входе в функцию они - обычные переменные. Можно использовать 271 | `return` без указания возвращаемых значений, и вернутся значения из этих переменных 272 | 273 | ## Функции 274 | 275 | * Функции можно использовать как одругие типы - записывать в переменные, передавать в аргументах и т.д. (они first-class values) 276 | * Однако, следует помнить, что переменные вне скоупа функции захватываются "по ссылке" 277 | * Например, нельзя захватывать переменные в цикле: 278 | 279 | var rmdirs []func() 280 | for _, d := range tempDirs() { 281 | dir := d // NOTE: necessary! 282 | os.MkdirAll(dir, 0755) // creates parent directories too 283 | rmdirs = append(rmdirs, func() { 284 | os.RemoveAll(dir) 285 | })g 286 | } 287 | 288 | ## Ошибки 289 | 290 | * Все ошибки возвращаются явным образом с типом встроенного интерфейса `error` (исключение - panic, о нем отдельно) 291 | 292 | type error interface { 293 | Error() string 294 | } 295 | 296 | * Можно создавать ошибки различных типов, главное, чтобы они удовлетворяли интерфейсу 297 | * Ошибки можно сравнивать с nil и между собой (устаревший вариант, после go 1.13 для сравнения используются `errors.Is` и `errors.As`) 298 | * Метод `errors.Unwrap` вызывает соответствующий метод у типа, если он определен, либо возвращает nil 299 | 300 | ## Ошибки 301 | 302 | * Ошибки могут оборачивать одна другую: `fmt.Errorf("failed: %w", ErrNotFound)` 303 | * `errors.Is` - в цепочке ошибок есть ошибка-переменная (`var ErrNotFound = errors.New("not found")`) 304 | * `errors.As` - в цепочке ошибок есть ошибки с нужным типом -------------------------------------------------------------------------------- /tasks/03/courses/applications_test.go: -------------------------------------------------------------------------------- 1 | package courses 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestProcessApplications(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | students []StudentApplication 13 | courses []Course 14 | expected map[CourseName][]StudentName 15 | }{ 16 | { 17 | name: "Single student, single course, within limit", 18 | students: []StudentApplication{ 19 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 20 | }, 21 | courses: []Course{ 22 | {Name: "Math", Limit: 1}, 23 | }, 24 | expected: map[CourseName][]StudentName{ 25 | "Math": {{FirstName: "John", LastName: "Doe"}}, 26 | }, 27 | }, 28 | { 29 | name: "Single student, single course, over limit", 30 | students: []StudentApplication{ 31 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 32 | }, 33 | courses: []Course{ 34 | {Name: "Math", Limit: 0}, 35 | }, 36 | expected: map[CourseName][]StudentName{}, 37 | }, 38 | { 39 | name: "Multiple students, single course, within limit", 40 | students: []StudentApplication{ 41 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Math"}}, 42 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 43 | }, 44 | courses: []Course{ 45 | {Name: "Math", Limit: 2}, 46 | }, 47 | expected: map[CourseName][]StudentName{ 48 | "Math": { 49 | {FirstName: "John", LastName: "Doe"}, 50 | {FirstName: "Jane", LastName: "Smith"}, 51 | }, 52 | }, 53 | }, 54 | { 55 | name: "Multiple students, single course, over limit", 56 | students: []StudentApplication{ 57 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Math"}}, 58 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 59 | }, 60 | courses: []Course{ 61 | {Name: "Math", Limit: 1}, 62 | }, 63 | expected: map[CourseName][]StudentName{ 64 | "Math": {{FirstName: "John", LastName: "Doe"}}, 65 | }, 66 | }, 67 | { 68 | name: "Multiple students, multiple courses, within limit", 69 | students: []StudentApplication{ 70 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Physics", "Math"}}, 71 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 72 | }, 73 | courses: []Course{ 74 | {Name: "Math", Limit: 1}, 75 | {Name: "Physics", Limit: 1}, 76 | }, 77 | expected: map[CourseName][]StudentName{ 78 | "Math": {{FirstName: "John", LastName: "Doe"}}, 79 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 80 | }, 81 | }, 82 | { 83 | name: "Multiple students, multiple courses, over limit", 84 | students: []StudentApplication{ 85 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 86 | {Name: StudentName{FirstName: "Alice", LastName: "Johnson"}, Avg: 80, Priorities: []CourseName{"Math", "Physics"}}, 87 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Physics", "Math"}}, 88 | }, 89 | courses: []Course{ 90 | {Name: "Math", Limit: 1}, 91 | {Name: "Physics", Limit: 1}, 92 | }, 93 | expected: map[CourseName][]StudentName{ 94 | "Math": {{FirstName: "John", LastName: "Doe"}}, 95 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 96 | }, 97 | }, 98 | { 99 | name: "Students with same average, different names", 100 | students: []StudentApplication{ 101 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 102 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 90, Priorities: []CourseName{"Math"}}, 103 | }, 104 | courses: []Course{ 105 | {Name: "Math", Limit: 2}, 106 | }, 107 | expected: map[CourseName][]StudentName{ 108 | "Math": { 109 | {FirstName: "John", LastName: "Doe"}, 110 | {FirstName: "Jane", LastName: "Smith"}, 111 | }, 112 | }, 113 | }, 114 | { 115 | name: "Students with same average and last name, different first names", 116 | students: []StudentApplication{ 117 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 118 | {Name: StudentName{FirstName: "Jane", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 119 | }, 120 | courses: []Course{ 121 | {Name: "Math", Limit: 2}, 122 | }, 123 | expected: map[CourseName][]StudentName{ 124 | "Math": { 125 | {FirstName: "Jane", LastName: "Doe"}, 126 | {FirstName: "John", LastName: "Doe"}, 127 | }, 128 | }, 129 | }, 130 | { 131 | name: "Students with different averages, same names", 132 | students: []StudentApplication{ 133 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math"}}, 134 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 85, Priorities: []CourseName{"Math"}}, 135 | }, 136 | courses: []Course{ 137 | {Name: "Math", Limit: 2}, 138 | }, 139 | expected: map[CourseName][]StudentName{ 140 | "Math": { 141 | {FirstName: "John", LastName: "Doe"}, 142 | {FirstName: "John", LastName: "Doe"}, 143 | }, 144 | }, 145 | }, 146 | { 147 | name: "Students with no priorities", 148 | students: []StudentApplication{ 149 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{}}, 150 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{}}, 151 | }, 152 | courses: []Course{ 153 | {Name: "Math", Limit: 2}, 154 | }, 155 | expected: map[CourseName][]StudentName{}, 156 | }, 157 | { 158 | name: "Courses with no students", 159 | students: []StudentApplication{}, 160 | courses: []Course{ 161 | {Name: "Math", Limit: 2}, 162 | {Name: "Physics", Limit: 1}, 163 | }, 164 | expected: map[CourseName][]StudentName{}, 165 | }, 166 | { 167 | name: "Students with non-existent course priorities", 168 | students: []StudentApplication{ 169 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Chemistry"}}, 170 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Biology"}}, 171 | }, 172 | courses: []Course{ 173 | {Name: "Math", Limit: 2}, 174 | {Name: "Physics", Limit: 1}, 175 | }, 176 | expected: map[CourseName][]StudentName{}, 177 | }, 178 | { 179 | name: "Students with multiple priorities, first priority full", 180 | students: []StudentApplication{ 181 | {Name: StudentName{FirstName: "Alice", LastName: "Johnson"}, Avg: 80, Priorities: []CourseName{"Math", "Physics"}}, 182 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 183 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Math", "Physics"}}, 184 | }, 185 | courses: []Course{ 186 | {Name: "Math", Limit: 1}, 187 | {Name: "Physics", Limit: 2}, 188 | }, 189 | expected: map[CourseName][]StudentName{ 190 | "Math": {{FirstName: "John", LastName: "Doe"}}, 191 | "Physics": { 192 | {FirstName: "Alice", LastName: "Johnson"}, 193 | {FirstName: "Jane", LastName: "Smith"}, 194 | }, 195 | }, 196 | }, 197 | { 198 | name: "Students with multiple priorities, second priority full", 199 | students: []StudentApplication{ 200 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 201 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Physics", "Math"}}, 202 | {Name: StudentName{FirstName: "Alice", LastName: "Johnson"}, Avg: 80, Priorities: []CourseName{"Physics", "Math"}}, 203 | }, 204 | courses: []Course{ 205 | {Name: "Math", Limit: 2}, 206 | {Name: "Physics", Limit: 1}, 207 | }, 208 | expected: map[CourseName][]StudentName{ 209 | "Math": {{FirstName: "John", LastName: "Doe"}, {FirstName: "Alice", LastName: "Johnson"}}, 210 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 211 | }, 212 | }, 213 | { 214 | name: "Students with multiple priorities, all priorities full", 215 | students: []StudentApplication{ 216 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 217 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Physics", "Math"}}, 218 | {Name: StudentName{FirstName: "Alice", LastName: "Johnson"}, Avg: 80, Priorities: []CourseName{"Math", "Physics"}}, 219 | }, 220 | courses: []Course{ 221 | {Name: "Math", Limit: 1}, 222 | {Name: "Physics", Limit: 1}, 223 | }, 224 | expected: map[CourseName][]StudentName{ 225 | "Math": {{FirstName: "John", LastName: "Doe"}}, 226 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 227 | }, 228 | }, 229 | { 230 | name: "Students with same average, different priorities", 231 | students: []StudentApplication{ 232 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 233 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 90, Priorities: []CourseName{"Physics", "Math"}}, 234 | }, 235 | courses: []Course{ 236 | {Name: "Math", Limit: 1}, 237 | {Name: "Physics", Limit: 1}, 238 | }, 239 | expected: map[CourseName][]StudentName{ 240 | "Math": {{FirstName: "John", LastName: "Doe"}}, 241 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 242 | }, 243 | }, 244 | { 245 | name: "Students with same average, same priorities", 246 | students: []StudentApplication{ 247 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 248 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 249 | }, 250 | courses: []Course{ 251 | {Name: "Math", Limit: 1}, 252 | {Name: "Physics", Limit: 1}, 253 | }, 254 | expected: map[CourseName][]StudentName{ 255 | "Math": {{FirstName: "John", LastName: "Doe"}}, 256 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 257 | }, 258 | }, 259 | { 260 | name: "Students with different averages, same priorities", 261 | students: []StudentApplication{ 262 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 263 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Math", "Physics"}}, 264 | }, 265 | courses: []Course{ 266 | {Name: "Math", Limit: 1}, 267 | {Name: "Physics", Limit: 1}, 268 | }, 269 | expected: map[CourseName][]StudentName{ 270 | "Math": {{FirstName: "John", LastName: "Doe"}}, 271 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 272 | }, 273 | }, 274 | { 275 | name: "Students with different averages, different priorities", 276 | students: []StudentApplication{ 277 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 278 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 85, Priorities: []CourseName{"Physics", "Math"}}, 279 | }, 280 | courses: []Course{ 281 | {Name: "Math", Limit: 1}, 282 | {Name: "Physics", Limit: 1}, 283 | }, 284 | expected: map[CourseName][]StudentName{ 285 | "Math": {{FirstName: "John", LastName: "Doe"}}, 286 | "Physics": {{FirstName: "Jane", LastName: "Smith"}}, 287 | }, 288 | }, 289 | { 290 | name: "Students with same average, same names, different priorities", 291 | students: []StudentApplication{ 292 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Math", "Physics"}}, 293 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 90, Priorities: []CourseName{"Physics", "Math"}}, 294 | }, 295 | courses: []Course{ 296 | {Name: "Math", Limit: 1}, 297 | {Name: "Physics", Limit: 1}, 298 | }, 299 | expected: map[CourseName][]StudentName{ 300 | "Math": {{FirstName: "John", LastName: "Doe"}}, 301 | "Physics": {{FirstName: "John", LastName: "Doe"}}, 302 | }, 303 | }, 304 | { 305 | name: "Big", 306 | students: []StudentApplication{ 307 | {Name: StudentName{FirstName: "Bob", LastName: "Brown"}, Avg: 88, Priorities: []CourseName{"Biology", "Chemistry", "Physics"}}, 308 | {Name: StudentName{FirstName: "Eve", LastName: "Garcia"}, Avg: 80, Priorities: []CourseName{"Biology", "Physics", "Chemistry"}}, 309 | {Name: StudentName{FirstName: "Hank", LastName: "Jones"}, Avg: 75, Priorities: []CourseName{"Physics", "Math", "Chemistry", "History"}}, 310 | {Name: StudentName{FirstName: "John", LastName: "Doe"}, Avg: 95, Priorities: []CourseName{"Math", "Physics", "Chemistry"}}, 311 | {Name: StudentName{FirstName: "Charlie", LastName: "Davis"}, Avg: 85, Priorities: []CourseName{"Math", "Biology", "Chemistry"}}, 312 | {Name: StudentName{FirstName: "Grace", LastName: "Jones"}, Avg: 75, Priorities: []CourseName{"Math", "Physics", "Biology", "History"}}, 313 | {Name: StudentName{FirstName: "Alice", LastName: "Johnson"}, Avg: 88, Priorities: []CourseName{"Chemistry", "Physics", "Math"}}, 314 | {Name: StudentName{FirstName: "Jane", LastName: "Smith"}, Avg: 95, Priorities: []CourseName{"Physics", "Math", "Biology"}}, 315 | {Name: StudentName{FirstName: "Frank", LastName: "Harris"}, Avg: 78, Priorities: []CourseName{"Chemistry", "Biology", "Physics"}}, 316 | {Name: StudentName{FirstName: "Diana", LastName: "Evans"}, Avg: 83, Priorities: []CourseName{"Physics", "Chemistry", "Math"}}, 317 | }, 318 | courses: []Course{ 319 | {Name: "Math", Limit: 2}, 320 | {Name: "Physics", Limit: 2}, 321 | {Name: "Chemistry", Limit: 2}, 322 | {Name: "Biology", Limit: 2}, 323 | {Name: "History", Limit: 1}, 324 | }, 325 | expected: map[CourseName][]StudentName{ 326 | "Math": { 327 | {FirstName: "Charlie", LastName: "Davis"}, 328 | {FirstName: "John", LastName: "Doe"}, 329 | }, 330 | "Physics": { 331 | {FirstName: "Diana", LastName: "Evans"}, 332 | {FirstName: "Jane", LastName: "Smith"}, 333 | }, 334 | "Chemistry": { 335 | {FirstName: "Frank", LastName: "Harris"}, 336 | {FirstName: "Alice", LastName: "Johnson"}, 337 | }, 338 | "Biology": { 339 | {FirstName: "Bob", LastName: "Brown"}, 340 | {FirstName: "Eve", LastName: "Garcia"}, 341 | }, 342 | "History": { 343 | {FirstName: "Grace", LastName: "Jones"}, 344 | }, 345 | }, 346 | }, 347 | } 348 | 349 | for _, tc := range testCases { 350 | t.Run(tc.name, func(t *testing.T) { 351 | result := ProcessApplications(tc.students, tc.courses) 352 | assert.Equal(t, tc.expected, result) 353 | }) 354 | } 355 | } 356 | --------------------------------------------------------------------------------